Carica le conversioni generate dalle vendite in negozio

L'API Google Ads supporta il caricamento delle vendite in negozio conversioni che ti consentono importare le transazioni offline in Google Ads direttamente o tramite terze parti partner. Abbinando i dati delle transazioni dal tuo sistema point of sale o database dei clienti, puoi vedere in che modo i tuoi annunci si traducono in acquisti offline.

Consulta il Centro assistenza per le vendite in negozio articoli e segui le elenco di controllo per a capire meglio cosa è necessario per usare le vendite in negozio.

Configurare l'account Google Ads per le vendite in negozio

Verifica la tua idoneità e collabora con il tuo rappresentante di Google per richiedere l'account configurarli in una lista consentita e creare azioni di conversione seguendo le istruzioni riportate in Configurare il tuo account Google Ads per il negozio vendite.

Puoi creare azioni di conversione per le vendite in negozio solo tramite il sito web di Google Ads a riga di comando. Durante il recupero le azioni di conversione tramite l'API Google Ads, ottieni ConversionAction con type come STORE_SALES

Il tipo di azione di conversione per le vendite in negozio può essere basato su dati proprietari e caricamenti di dati di commercianti di terze parti. STORE_SALES può includere acquisti in negozio utilizzando carte di reti di pagamento.

Creare variabili personalizzate

Puoi creare modelli variabili per le vendite in negozio l'azione di conversione e carica le transazioni offline nel i valori delle variabili. Questa funzione ti consente di caricare i dati delle vendite offline livello più dettagliato e per vedere le conversioni da online a offline e il ROI al le sezioni più importanti per la tua attività.

Devi creare una variabile personalizzata per le vendite in negozio e i relativi valori della variabile personalizzata utilizzando l'interfaccia web di Google Ads prima di eseguire il caricamento. Ogni transazione deve essere caricati con al massimo un valore di variabile personalizzata, altrimenti i report potrebbero verificarsi inesattezze.

Quando carichi i dati delle vendite in negozio con una variabile personalizzata utilizzando l'API Google Ads, devi specificare la variabile personalizzata Attributo custom_key in StoreSalesMetadata e fornire il segmento di pubblico personalizzato come valore della variabile custom_value nel TransactionAttribute.

Gli errori che potresti riscontrare nell'API variabile personalizzata includono i seguenti:

Carica i dati delle vendite in negozio

Dopo aver creato l'azione di conversione, puoi iniziare a caricare il negozio conversioni di vendita utilizzando l'interfaccia web o l'API. Procedi nel seguente modo:

  1. Devi impostare il campo customer_id nel Richieste OfflineUserDataJobService all'ID cliente della conversione di Google Ads cliente di nel tuo account Google Ads.
  2. Determina se sei un utente di terze parti o proprietario. Se disponi di un indirizzo partnership di vendita in negozio con Google, allora sei una terza parte. Altrimenti, sono dati proprietari.
  3. Crea una OfflineUserDataJob. Se sono di terze parti, imposta il tipo di job su STORE_SALES_UPLOAD_THIRD_PARTY. Altrimenti, usa STORE_SALES_UPLOAD_FIRST_PARTY. Compila solo StoreSalesMetadata.third_party_metadata se si tratta di un job di terze parti.
  4. Aggiungi le transazioni di vendita in negozio al job. Devi associare il negozio conversioni di vendita con l'azione di conversione trasmettendo la conversione nome risorsa dell'azione e archiviare le transazioni UserData e aggiungerlo a un oggetto creato OfflineUserDataJob tramite OfflineUserDataJobService. A partire dalla versione v15 dell'API Google Ads, devi compilare la Campo consent dell'oggetto UserData. Gli inserzionisti devono trasmettere i dati delle vendite in negozio con valori relativi al consenso per entrambi gli utenti degli annunci e personalizzazione degli annunci Utenti dello Spazio economico europeo (SEE) per conservare l'accesso alle funzionalità Customer Match. Solo i dati con saranno utilizzabili per Customer Match con le vendite in negozio. Esamina il Domande frequenti per ulteriori informazioni.
  5. Esegui il job asincrono chiamando RunOfflineUserDataJob Il valore status del job passa da PENDING a RUNNING all'avvio.
  6. Il completamento dei job dei dati utente offline può richiedere fino a 24 ore. Puoi controllare periodicamente lo stato del lavoro tramite GoogleAdsService.SearchStream Il job è stato completato quando il relativo status diventa SUCCESS o FAILED.

Requisiti

Quando si aggiunge un oggetto, è necessario soddisfare diversi dettagli e requisiti UserData si oppone a un OfflineUserDataJob.

Per evitare un errore OfflineUserDataJobError.INVALID_CONVERSION_ACTION, il valore conversion_action deve fare riferimento a ConversionAction con le seguenti condizioni:

  • Il status del ConversionAction è ENABLED.
  • Il type è STORE_SALES.
  • ConversionAction esiste nel cliente Google Ads che ha effettuato la conversione dell'account l'account Google Ads di un clic.

Quando carichi le transazioni di vendita in negozio, si applicano i seguenti limiti:

  • La raccolta operations per ogni AddOfflineUserDataJobOperationsRequest può contenere al massimo 100.000 identificatori in tutti gli oggetti UserData nelle operazioni.
  • Non sono previsti limiti al numero di operazioni che puoi aggiungere a un singolo job. Tuttavia, per un'elaborazione ottimale, consigliamo di aggiungere fino a 10.000 operazioni in una singola chiamata OfflineUserDataJobService.AddOfflineUserDataJobOperations e fino a 1.000.000 di operazioni per un singolo job.
  • I job di caricamento con transazioni insufficienti potrebbero non riuscire con OfflineUserDataJobFailureReason.INSUFFICIENT_MATCHED_TRANSACTIONS. Carica almeno alcune centinaia di transazioni per aumentare la probabilità che il tuo job raggiunge le soglie pertinenti.

Inoltre, devono essere soddisfatte le seguenti condizioni:

  • Al momento del clic, il monitoraggio delle conversioni era attivato nella Conversione di Google Ads cliente di l'account Google Ads del clic.
  • Il valore transaction_amount_micros deve essere maggiore di zero.
  • Per transaction_date_time può essere specificato un fuso orario facoltativo. e il formato è yyyy-mm-dd HH:mm:ss+|-HH:mm, ad esempio: 2022-01-01 19:32:45-05:00 (ignorando l'ora legale) di Google. Il fuso orario può riferirsi a qualsiasi value: non deve corrispondere al fuso orario dell'account; dovrebbe essere il fuso orario in cui è avvenuta la transazione. Quando non viene fornito il fuso orario, viene utilizzato il fuso orario dell'account.
  • Dati come email, numeri di telefono e nomi devono essere normalizzati e sottoposti ad hashing utilizzando SHA-256.

Esempio di codice

Java

// Copyright 2020 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.remarketing;

import com.beust.jcommander.Parameter;
import com.google.ads.googleads.examples.utils.ArgumentNames;
import com.google.ads.googleads.examples.utils.CodeSampleParams;
import com.google.ads.googleads.lib.GoogleAdsClient;
import com.google.ads.googleads.v17.common.Consent;
import com.google.ads.googleads.v17.common.OfflineUserAddressInfo;
import com.google.ads.googleads.v17.common.StoreSalesMetadata;
import com.google.ads.googleads.v17.common.StoreSalesThirdPartyMetadata;
import com.google.ads.googleads.v17.common.TransactionAttribute;
import com.google.ads.googleads.v17.common.UserData;
import com.google.ads.googleads.v17.common.UserIdentifier;
import com.google.ads.googleads.v17.enums.ConsentStatusEnum.ConsentStatus;
import com.google.ads.googleads.v17.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus;
import com.google.ads.googleads.v17.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType;
import com.google.ads.googleads.v17.errors.GoogleAdsError;
import com.google.ads.googleads.v17.errors.GoogleAdsException;
import com.google.ads.googleads.v17.errors.GoogleAdsFailure;
import com.google.ads.googleads.v17.resources.OfflineUserDataJob;
import com.google.ads.googleads.v17.services.AddOfflineUserDataJobOperationsRequest;
import com.google.ads.googleads.v17.services.AddOfflineUserDataJobOperationsResponse;
import com.google.ads.googleads.v17.services.CreateOfflineUserDataJobResponse;
import com.google.ads.googleads.v17.services.GoogleAdsRow;
import com.google.ads.googleads.v17.services.GoogleAdsServiceClient;
import com.google.ads.googleads.v17.services.OfflineUserDataJobOperation;
import com.google.ads.googleads.v17.services.OfflineUserDataJobServiceClient;
import com.google.ads.googleads.v17.utils.ErrorUtils;
import com.google.ads.googleads.v17.utils.ResourceNames;
import com.google.common.collect.ImmutableList;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * Uploads offline data for store sales transactions.
 *
 * <p>This feature is only available to allowlisted accounts. See
 * https://support.google.com/google-ads/answer/7620302 for more details.
 */
public class UploadStoreSalesTransactions {

  private static class UploadStoreSalesTransactionsParams extends CodeSampleParams {

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

    @Parameter(
        names = ArgumentNames.OFFLINE_USER_DATA_JOB_TYPE,
        required = false,
        description =
            "The type of user data in the job (first or third party). If you have an official"
                + " store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY."
                + " Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.")
    private OfflineUserDataJobType offlineUserDataJobType =
        OfflineUserDataJobType.STORE_SALES_UPLOAD_FIRST_PARTY;

    @Parameter(
        names = ArgumentNames.EXTERNAL_ID,
        description =
            "Optional (but recommended) external ID to identify the offline user data job")
    private Long externalId;

    @Parameter(
        names = ArgumentNames.CONVERSION_ACTION_ID,
        required = true,
        description = "The ID of a store sales conversion action")
    private Long conversionActionId;

    @Parameter(
        names = ArgumentNames.CUSTOM_KEY,
        required = false,
        description =
            "Only required after creating a custom key and custom values in the account."
                + " Custom key and values are used to segment store sales conversions."
                + " This measurement can be used to provide more advanced insights.")
    private String customKey;

    @Parameter(
        names = ArgumentNames.ADVERTISER_UPLOAD_DATE_TIME,
        description = "Only required if uploading third party data")
    private String advertiserUploadDateTime;

    @Parameter(
        names = ArgumentNames.BRIDGE_MAP_VERSION_ID,
        description = "Only required if uploading third party data")
    private String bridgeMapVersionId;

    @Parameter(
        names = ArgumentNames.PARTNER_ID,
        description = "Only required if uploading third party data")
    private Long partnerId;

    @Parameter(
        names = ArgumentNames.ITEM_ID,
        description =
            "Specify a unique identifier of a product, either the Merchant Center Item ID or"
                + " Global Trade Item Number (GTIN). Only required if uploading with item"
                + " attributes.")
    private String itemId;

    @Parameter(
        names = ArgumentNames.MERCHANT_CENTER_ACCOUNT_ID,
        description =
            "A Merchant Center Account ID. Only required if uploading with item attributes.")
    private Long merchantCenterAccountId;

    @Parameter(
        names = ArgumentNames.COUNTRY_CODE,
        description =
            "A two-letter country code of the location associated with the feed where your items"
                + " are uploaded. Only required if uploading with item attributes. For a list of"
                + " country codes see the country codes here:"
                + " https://developers.google.com/google-ads/api/reference/data/codes-formats#country-codes")
    private String countryCode;

    @Parameter(
        names = ArgumentNames.LANGUAGE_CODE,
        description =
            "A two-letter language code of the language associated with the feed where your items"
                + " are uploaded. Only required if uploading with item attributes. For a list of"
                + " language codes see:"
                + " https://developers.google.com/google-ads/api/reference/data/codes-formats#languages")
    private String languageCode;

    @Parameter(
        names = ArgumentNames.QUANTITY,
        description =
            "The number of items sold. Can only be set when at least one other item attribute has"
                + " been provided. Only required if uploading with item attributes.")
    private int quantity;

    @Parameter(names = ArgumentNames.AD_PERSONALIZATION_CONSENT, required = false)
    private ConsentStatus adPersonalizationConsent;

    @Parameter(names = ArgumentNames.AD_USER_DATA_CONSENT, required = false)
    private ConsentStatus adUserDataConsent;
  }

  /** Specifies the value to use if uploading data with custom key and values. */
  private static final String CUSTOM_VALUE = null;

  public static void main(String[] args)
      throws InterruptedException,
          ExecutionException,
          TimeoutException,
          UnsupportedEncodingException {
    UploadStoreSalesTransactionsParams params = new UploadStoreSalesTransactionsParams();
    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.offlineUserDataJobType =
          OfflineUserDataJobType.valueOf("INSERT_OFFLINE_USER_DATA_JOB_TYPE_HERE");
      params.conversionActionId = Long.parseLong("INSERT_CONVERSION_ACTION_ID_HERE");
      // OPTIONAL (but recommended): Specify an external ID for the job.
      // params.externalId = Long.parseLong("INSERT_EXTERNAL_ID_HERE");

      // OPTIONAL: specify the ad user data consent.
      // params.adUserDataConsent = ConsentStatus.valueOf("INSERT_AD_USER_DATA_CONSENT_HERE");

      // OPTIONAL: If uploading data with custom key and values, also specify the following value:
      // params.customKey = "INSERT_CUSTOM_KEY_HERE";

      // OPTIONAL: If uploading third party data, also specify the following values:
      // params.advertiserUploadDateTime = "INSERT_ADVERTISER_UPLOAD_DATE_TIME_HERE";
      // params.bridgeMapVersionId = "INSERT_BRIDGE_MAP_VERSION_ID_HERE";
      // params.partnerId = Long.parseLong("INSERT_PARTNER_ID_HERE");

      // OPTIONAL: Specify a unique identifier of a product, either the Merchant Center
      // Item ID or Global Trade Item Number (GTIN). Only required if uploading with
      // item attributes.
      // params.itemId = Long.parseLong("INSERT_ITEM_ID_HERE");

      // OPTIONAL: Specify a Merchant Center Account ID. Only required if uploading
      // with item attributes.
      // params.merchantCenterAccountId = Long.parseLong("INSERT_MERCHANT_CENTER_ID_HERE");

      // OPTIONAL: Specify a two-letter country code of the location associated with the
      // feed where your items are uploaded. Only required if uploading with item
      // attributes.
      // params.countryCode = "INSERT_COUNTRY_CODE_HERE";

      // OPTIONAL: Specify a two-letter language code of the language associated with
      // the feed where your items are uploaded. Only required if uploading with item
      // attributes.
      // params.languageCode = "INSERT_LANGUAGE_CODE_HERE";

      // OPTIONAL: Specify a number of items sold. Only required if uploading with item
      // attributes.
      // params.quantity = 1;
    }

    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 UploadStoreSalesTransactions()
          .runExample(
              googleAdsClient,
              params.customerId,
              params.offlineUserDataJobType,
              params.externalId,
              params.conversionActionId,
              params.adPersonalizationConsent,
              params.adUserDataConsent,
              params.customKey,
              params.advertiserUploadDateTime,
              params.bridgeMapVersionId,
              params.partnerId,
              params.itemId,
              params.merchantCenterAccountId,
              params.countryCode,
              params.languageCode,
              params.quantity);
    } 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 offlineUserDataJobType the type of offline user data in the job (first party or third
   *     party). If you have an official store sales partnership with Google, use {@code
   *     STORE_SALES_UPLOAD_THIRD_PARTY}. Otherwise, use {@code STORE_SALES_UPLOAD_FIRST_PARTY}.
   * @param externalId optional (but recommended) external ID for the offline user data job.
   * @param conversionActionId the ID of a store sales conversion action.
   * @param adPersonalizationConsent the ad personalization consent status.
   * @param adUserDataConsent the ad user data consent status.
   * @param customKey to segment store sales conversions. Only required after creating a custom key
   *     and custom values in the account.
   * @param advertiserUploadDateTime date and time the advertiser uploaded data to the partner. Only
   *     required for third party uploads.
   * @param bridgeMapVersionId version of partner IDs to be used for uploads. Only required for
   *     third party uploads.
   * @param partnerId ID of the third party partner. Only required for third party uploads.
   * @param itemId the ID of the item in merchant center (optional).
   * @param merchantCenterAccountId the ID of the merchant center account (optional).
   * @param countryCode the country code of the item for sale in merchant center.
   * @param languageCode the language of the item for sale in merchant center.
   * @param quantity the number of items that we sold.
   * @throws GoogleAdsException if an API request failed with one or more service errors.
   */
  private void runExample(
      GoogleAdsClient googleAdsClient,
      long customerId,
      OfflineUserDataJobType offlineUserDataJobType,
      Long externalId,
      long conversionActionId,
      ConsentStatus adPersonalizationConsent,
      ConsentStatus adUserDataConsent,
      String customKey,
      String advertiserUploadDateTime,
      String bridgeMapVersionId,
      Long partnerId,
      String itemId,
      Long merchantCenterAccountId,
      String countryCode,
      String languageCode,
      int quantity)
      throws InterruptedException,
          ExecutionException,
          TimeoutException,
          UnsupportedEncodingException {
    String offlineUserDataJobResourceName;
    try (OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
        googleAdsClient.getLatestVersion().createOfflineUserDataJobServiceClient()) {
      // Creates an offline user data job for uploading transactions.
      offlineUserDataJobResourceName =
          createOfflineUserDataJob(
              offlineUserDataJobServiceClient,
              customerId,
              offlineUserDataJobType,
              externalId,
              customKey,
              advertiserUploadDateTime,
              bridgeMapVersionId,
              partnerId);

      // Adds transactions to the job.
      addTransactionsToOfflineUserDataJob(
          offlineUserDataJobServiceClient,
          customerId,
          offlineUserDataJobResourceName,
          conversionActionId,
          adPersonalizationConsent,
          adUserDataConsent,
          customKey,
          itemId,
          merchantCenterAccountId,
          countryCode,
          languageCode,
          quantity);

      // Issues an asynchronous request to run the offline user data job.
      offlineUserDataJobServiceClient.runOfflineUserDataJobAsync(offlineUserDataJobResourceName);

      // BEWARE! The above call returns an OperationFuture. The execution of that future depends on
      // the thread pool which is owned by offlineUserDataJobServiceClient. If you use this future,
      // you *must* keep the service client in scope too.
      // See https://developers.google.com/google-ads/api/docs/client-libs/java/lro for more detail.

      System.out.printf(
          "Sent request to asynchronously run offline user data job: %s%n",
          offlineUserDataJobResourceName);
    }

    // Offline user data jobs may take up to 24 hours to complete, so instead of waiting for the job
    // to complete, retrieves and displays the job status once and then prints the query to use to
    // check the job again later.
    checkJobStatus(googleAdsClient, customerId, offlineUserDataJobResourceName);
  }

  /**
   * Creates an offline user data job for uploading store sales transactions.
   *
   * @return the resource name of the created job.
   */
  private String createOfflineUserDataJob(
      OfflineUserDataJobServiceClient offlineUserDataJobServiceClient,
      long customerId,
      OfflineUserDataJobType offlineUserDataJobType,
      Long externalId,
      String customKey,
      String advertiserUploadDateTime,
      String bridgeMapVersionId,
      Long partnerId) {
    // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses the
    // term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is called
    // loyaltyFraction in the Google Ads API.
    StoreSalesMetadata.Builder storeSalesMetadataBuilder =
        // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
        // details.
        StoreSalesMetadata.newBuilder()
            // Sets the fraction of your overall sales that you (or the advertiser, in the third
            // party case) can associate with a customer (email, phone number, address, etc.) in
            // your database or loyalty program.
            // For example, set this to 0.7 if you have 100 transactions over 30 days, and out of
            // those 100 transactions, you can identify 70 by an email address or phone number.
            .setLoyaltyFraction(0.7)
            // Sets the fraction of sales you're uploading out of the overall sales that you (or the
            // advertiser, in the third party case) can associate with a customer. In most cases,
            // you will set this to 1.0.
            // Continuing the example above for loyalty fraction, a value of 1.0 here indicates that
            // you are uploading all 70 of the transactions that can be identified by an email
            // address or phone number.
            .setTransactionUploadFraction(1.0);

    if (customKey != null && !customKey.isEmpty()) {
      storeSalesMetadataBuilder.setCustomKey(customKey);
    }

    if (OfflineUserDataJobType.STORE_SALES_UPLOAD_THIRD_PARTY == offlineUserDataJobType) {
      // Creates additional metadata required for uploading third party data.
      StoreSalesThirdPartyMetadata storeSalesThirdPartyMetadata =
          StoreSalesThirdPartyMetadata.newBuilder()
              // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
              .setAdvertiserUploadDateTime(advertiserUploadDateTime)

              // Sets the fraction of transactions you received from the advertiser that have valid
              // formatting and values. This captures any transactions the advertiser provided to
              // you but which you are unable to upload to Google due to formatting errors or
              // missing data.
              // In most cases, you will set this to 1.0.
              .setValidTransactionFraction(1.0)
              // Sets the fraction of valid transactions (as defined above) you received from the
              // advertiser that you (the third party) have matched to an external user ID on your
              // side.
              // In most cases, you will set this to 1.0.
              .setPartnerMatchFraction(1.0)

              // Sets the fraction of transactions you (the third party) are uploading out of the
              // transactions you received from the advertiser that meet both of the following
              // criteria:
              // 1. Are valid in terms of formatting and values. See valid transaction fraction
              // above.
              // 2. You matched to an external user ID on your side. See partner match fraction
              // above.
              // In most cases, you will set this to 1.0.
              .setPartnerUploadFraction(1.0)

              // Please speak with your Google representative to get the values to use for the
              // bridge map version and partner IDs.

              // Sets the version of partner IDs to be used for uploads.
              .setBridgeMapVersionId(bridgeMapVersionId)
              // Sets the third party partner ID uploading the transactions.
              .setPartnerId(partnerId)
              .build();
      storeSalesMetadataBuilder.setThirdPartyMetadata(storeSalesThirdPartyMetadata);
    }

    // Creates a new offline user data job.
    OfflineUserDataJob.Builder offlineUserDataJobBuilder =
        OfflineUserDataJob.newBuilder()
            .setType(offlineUserDataJobType)
            .setStoreSalesMetadata(storeSalesMetadataBuilder);
    if (externalId != null) {
      offlineUserDataJobBuilder.setExternalId(externalId);
    }

    // Issues a request to create the offline user data job.
    CreateOfflineUserDataJobResponse createOfflineUserDataJobResponse =
        offlineUserDataJobServiceClient.createOfflineUserDataJob(
            Long.toString(customerId), offlineUserDataJobBuilder.build());
    String offlineUserDataJobResourceName = createOfflineUserDataJobResponse.getResourceName();
    System.out.printf(
        "Created an offline user data job with resource name: %s.%n",
        offlineUserDataJobResourceName);
    return offlineUserDataJobResourceName;
  }

  /** Adds operations to the job for a set of sample transactions. */
  private void addTransactionsToOfflineUserDataJob(
      OfflineUserDataJobServiceClient offlineUserDataJobServiceClient,
      long customerId,
      String offlineUserDataJobResourceName,
      long conversionActionId,
      ConsentStatus adPersonalizationConsent,
      ConsentStatus adUserDataConsent,
      String customKey,
      String itemId,
      Long merchantId,
      String countryCode,
      String languageCode,
      Integer quantity)
      throws InterruptedException,
          ExecutionException,
          TimeoutException,
          UnsupportedEncodingException {
    // Constructs the operation for each transaction.
    List<OfflineUserDataJobOperation> userDataJobOperations =
        buildOfflineUserDataJobOperations(
            customerId,
            conversionActionId,
            adPersonalizationConsent,
            adUserDataConsent,
            customKey,
            itemId,
            merchantId,
            countryCode,
            languageCode,
            quantity);

    // Issues a request to add the operations to the offline user data job.
    AddOfflineUserDataJobOperationsResponse response =
        offlineUserDataJobServiceClient.addOfflineUserDataJobOperations(
            AddOfflineUserDataJobOperationsRequest.newBuilder()
                .setResourceName(offlineUserDataJobResourceName)
                .setEnablePartialFailure(true)
                // Enables warnings (optional).
                .setEnableWarnings(true)
                .addAllOperations(userDataJobOperations)
                .build());

    // Prints the status message if any partial failure error is returned.
    // NOTE: The details of each partial failure error are not printed here, you can refer to
    // the example HandlePartialFailure.java to learn more.
    if (response.hasPartialFailureError()) {
      GoogleAdsFailure googleAdsFailure =
          ErrorUtils.getInstance().getGoogleAdsFailure(response.getPartialFailureError());
      googleAdsFailure
          .getErrorsList()
          .forEach(e -> System.out.println("Partial failure occurred: " + e.getMessage()));
      System.out.printf(
          "Encountered %d partial failure errors while adding %d operations to the offline user "
              + "data job: '%s'. Only the successfully added operations will be executed when "
              + "the job runs.%n",
          ErrorUtils.getInstance().getFailedOperationIndices(googleAdsFailure).size(),
          userDataJobOperations.size(),
          response.getPartialFailureError().getMessage());

      // Checks if any warnings occurred and displays details.
      if (response.hasWarning()) {
        // Converts the Any in response back to a GoogleAdsFailure object.
        GoogleAdsFailure warningsFailure =
            ErrorUtils.getInstance().getGoogleAdsFailure(response.getWarning());
        // Prints some information about the warnings encountered.
        System.out.println(
            System.out.printf("Encountered %d warning(s).%n", warningsFailure.getErrorsCount()));
      }
    } else {
      System.out.printf(
          "Successfully added %d operations to the offline user data job.%n",
          userDataJobOperations.size());
    }
  }

  /**
   * Creates a list of offline user data job operations for sample transactions.
   *
   * @return a list of operations.
   */
  private List<OfflineUserDataJobOperation> buildOfflineUserDataJobOperations(
      long customerId,
      long conversionActionId,
      ConsentStatus adPersonalizationConsent,
      ConsentStatus adUserDataConsent,
      String customKey,
      String itemId,
      Long merchantId,
      String countryCode,
      String languageCode,
      Integer quantity)
      throws UnsupportedEncodingException {
    MessageDigest sha256Digest;
    try {
      // Gets a digest for generating hashed values using SHA-256. You must normalize and hash the
      // the value for any field where the name begins with "hashed". See the normalizeAndHash()
      // method.
      sha256Digest = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("Missing SHA-256 algorithm implementation", e);
    }

    // Create the first transaction for upload based on an email address and state.
    UserData.Builder userDataWithEmailAddress =
        UserData.newBuilder()
            .addAllUserIdentifiers(
                ImmutableList.of(
                    UserIdentifier.newBuilder()
                        .setHashedEmail(
                            // Email addresses must be normalized and hashed.
                            normalizeAndHash(sha256Digest, "dana@example.com"))
                        .build(),
                    UserIdentifier.newBuilder()
                        .setAddressInfo(OfflineUserAddressInfo.newBuilder().setState("NY"))
                        .build()))
            .setTransactionAttribute(
                TransactionAttribute.newBuilder()
                    .setConversionAction(
                        ResourceNames.conversionAction(customerId, conversionActionId))
                    .setCurrencyCode("USD")
                    // Converts the transaction amount from $200 USD to micros.
                    .setTransactionAmountMicros(200L * 1_000_000L)
                    // Specifies the date and time of the transaction. The format is
                    // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                    // timezone offset from UTC. If the offset is absent, the API will
                    // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                    // or "2018-02-01 14:34:30+03:00".
                    .setTransactionDateTime("2020-05-01 23:52:12"));

    // Adds consent information if specified.
    if (adPersonalizationConsent != null || adUserDataConsent != null) {
      Consent.Builder consentBuilder = Consent.newBuilder();
      if (adPersonalizationConsent != null) {
        consentBuilder.setAdPersonalization(adPersonalizationConsent);
      }
      if (adUserDataConsent != null) {
        consentBuilder.setAdUserData(adUserDataConsent);
      }
      // Specifies whether user consent was obtained for the data you are uploading. See
      // https://www.google.com/about/company/user-consent-policy for details.
      userDataWithEmailAddress.setConsent(consentBuilder);
    }

    // Optional: If uploading data with custom key and values, also assign the custom value.
    if (customKey != null) {
      userDataWithEmailAddress.getTransactionAttributeBuilder().setCustomValue(CUSTOM_VALUE);
    }

    // Creates the second transaction for upload based on a physical address.
    UserData.Builder userDataWithPhysicalAddress =
        UserData.newBuilder()
            .addUserIdentifiers(
                UserIdentifier.newBuilder()
                    .setAddressInfo(
                        OfflineUserAddressInfo.newBuilder()
                            .setHashedFirstName(normalizeAndHash(sha256Digest, "Dana"))
                            .setHashedLastName(normalizeAndHash(sha256Digest, "Quinn"))
                            .setCountryCode("US")
                            .setPostalCode("10011")))
            .setTransactionAttribute(
                TransactionAttribute.newBuilder()
                    .setConversionAction(
                        ResourceNames.conversionAction(customerId, conversionActionId))
                    .setCurrencyCode("EUR")
                    // Converts the transaction amount from 450 EUR to micros.
                    .setTransactionAmountMicros(450L * 1_000_000L)
                    // Specifies the date and time of the transaction. This date and time will be
                    // interpreted by the API using the Google Ads customer's time zone.
                    // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                    .setTransactionDateTime("2020-05-14 19:07:02"));

    if (itemId != null) {
      userDataWithPhysicalAddress
          .getTransactionAttributeBuilder()
          .getItemAttributeBuilder()
          .setItemId(itemId)
          .setMerchantId(merchantId)
          .setCountryCode(countryCode)
          .setLanguageCode(languageCode)
          .setQuantity(quantity);
    }

    // Creates the operations to add the two transactions.
    List<OfflineUserDataJobOperation> operations = new ArrayList<>();
    for (UserData userData :
        Arrays.asList(userDataWithEmailAddress.build(), userDataWithPhysicalAddress.build())) {
      operations.add(OfflineUserDataJobOperation.newBuilder().setCreate(userData).build());
    }

    return operations;
  }

  /**
   * Returns the result of normalizing and then hashing the string using the provided digest.
   * Private customer data must be hashed during upload, as described at
   * https://support.google.com/google-ads/answer/7506124.
   *
   * @param digest the digest to use to hash the normalized string.
   * @param s the string to normalize and hash.
   */
  private String normalizeAndHash(MessageDigest digest, String s)
      throws UnsupportedEncodingException {
    // Normalizes by removing leading and trailing whitespace and converting all characters to
    // lower case.
    String normalized = s.trim().toLowerCase();
    // Hashes the normalized string using the hashing algorithm.
    byte[] hash = digest.digest(normalized.getBytes("UTF-8"));
    StringBuilder result = new StringBuilder();
    for (byte b : hash) {
      result.append(String.format("%02x", b));
    }

    return result.toString();
  }

  /** Retrieves, checks, and prints the status of the offline user data job. */
  private void checkJobStatus(
      GoogleAdsClient googleAdsClient, long customerId, String offlineUserDataJobResourceName) {
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      String query =
          String.format(
              "SELECT offline_user_data_job.resource_name, "
                  + "offline_user_data_job.id, "
                  + "offline_user_data_job.status, "
                  + "offline_user_data_job.type, "
                  + "offline_user_data_job.failure_reason "
                  + "FROM offline_user_data_job "
                  + "WHERE offline_user_data_job.resource_name = '%s'",
              offlineUserDataJobResourceName);
      // Issues the query and gets the GoogleAdsRow containing the job from the response.
      GoogleAdsRow googleAdsRow =
          googleAdsServiceClient
              .search(Long.toString(customerId), query)
              .iterateAll()
              .iterator()
              .next();
      OfflineUserDataJob offlineUserDataJob = googleAdsRow.getOfflineUserDataJob();
      System.out.printf(
          "Offline user data job ID %d with type '%s' has status: %s%n",
          offlineUserDataJob.getId(), offlineUserDataJob.getType(), offlineUserDataJob.getStatus());
      OfflineUserDataJobStatus jobStatus = offlineUserDataJob.getStatus();
      if (OfflineUserDataJobStatus.FAILED == jobStatus) {
        System.out.printf("  Failure reason: %s%n", offlineUserDataJob.getFailureReason());
      } else if (OfflineUserDataJobStatus.PENDING == jobStatus
          || OfflineUserDataJobStatus.RUNNING == jobStatus) {
        System.out.println();
        System.out.printf(
            "To check the status of the job periodically, use the following GAQL query with"
                + " GoogleAdsService.search:%n%s%n",
            query);
      }
    }
  }
}

      

C#

// Copyright 2020 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using CommandLine;
using Google.Ads.Gax.Examples;
using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.V17.Common;
using Google.Ads.GoogleAds.V17.Errors;
using Google.Ads.GoogleAds.V17.Resources;
using Google.Ads.GoogleAds.V17.Services;
using static Google.Ads.GoogleAds.V17.Enums.ConsentStatusEnum.Types;
using static Google.Ads.GoogleAds.V17.Enums.OfflineUserDataJobStatusEnum.Types;
using static Google.Ads.GoogleAds.V17.Enums.OfflineUserDataJobTypeEnum.Types;

namespace Google.Ads.GoogleAds.Examples.V17
{
    /// <summary>
    /// This code example uploads offline data for store sales transactions.
    /// This feature is only available to allowlisted accounts. See
    /// https://support.google.com/google-ads/answer/7620302 for more details.
    /// </summary>
    public class UploadStoreSalesTransactions : ExampleBase
    {
        /// <summary>
        /// Command line options for running the <see cref="UploadStoreSalesTransactions"/> example.
        /// </summary>
        public class Options : OptionsBase
        {
            /// <summary>
            /// The Google Ads customer ID for which the call is made.
            /// </summary>
            [Option("customerId", Required = true, HelpText =
                "The Google Ads customer ID for which the call is made.")]
            public long CustomerId { get; set; }

            /// <summary>
            /// The ID of a store sales conversion action.
            /// </summary>
            [Option("conversionActionId", Required = true, HelpText =
                "The ID of a store sales conversion action.")]
            public long ConversionActionId { get; set; }

            /// <summary>
            /// The type of user data in the job (first or third party). If you have an official
            /// store sales partnership with Google, use StoreSalesUploadThirdParty. Otherwise,
            /// use StoreSalesUploadFirstParty or omit this parameter.
            /// </summary>
            [Option("offlineUserDataJobType", Required = false, HelpText =
                    "The type of user data in the job (first or third party). If you have an" +
                    " official store sales partnership with Google, use " +
                    "StoreSalesUploadThirdParty. Otherwise, use StoreSalesUploadFirstParty or " +
                    "omit this parameter.",
                Default = OfflineUserDataJobType.StoreSalesUploadFirstParty)]
            public OfflineUserDataJobType OfflineUserDataJobType { get; set; }

            /// <summary>
            /// Optional (but recommended) external ID to identify the offline user data job.
            /// </summary>
            [Option("externalId", Required = false, HelpText =
                    "Optional (but recommended) external ID to identify the offline user data job.",
                Default = null)]
            public long? ExternalId { get; set; }

            /// <summary>
            /// Date and time the advertiser uploaded data to the partner. Only required if
            /// uploading third party data.
            /// </summary>
            [Option("advertiserUploadDateTime", Required = false, HelpText =
                "Date and time the advertiser uploaded data to the partner. Only required if " +
                "uploading third party data.", Default = null)]
            public string AdvertiserUploadDateTime { get; set; }

            /// <summary>
            /// Version of partner IDs to be used for uploads. Only required if uploading third
            /// party data.
            /// </summary>
            [Option("bridgeMapVersionId", Required = false, HelpText =
                "Version of partner IDs to be used for uploads. Only required if uploading " +
                "third party data.", Default = null)]
            public string BridgeMapVersionId { get; set; }

            /// <summary>
            /// ID of the third party partner. Only required if uploading third party data.
            /// </summary>
            [Option("partnerId", Required = false, HelpText =
                    "ID of the third party partner. Only required if uploading third party data.",
                Default = null)]
            public long? PartnerId { get; set; }

            /// <summary>
            /// Optional custom key name. Only required if uploading data with custom key and
            /// values.
            /// </summary>
            [Option("customKey", Required = false, HelpText =
                "Optional custom key name. Only required if uploading data with custom key and" +
                " values.", Default = null)]
            public string CustomKey { get; set; }

            /// <summary>
            /// A unique identifier of a product, either the Merchant Center Item ID or Global Trade
            /// Item Number (GTIN). Only required if uploading with item attributes.
            /// </summary>
            [Option("itemId", Required = false, HelpText =
                    "A unique identifier of a product, either the Merchant Center Item ID or " +
                    "Global Trade Item Number (GTIN). Only required if uploading with item " +
                    "attributes.",
                Default = null)]
            public string ItemId { get; set; }

            /// <summary>
            /// A Merchant Center Account ID. Only required if uploading with item attributes.
            /// </summary>
            [Option("merchantCenterAccountId", Required = false, HelpText =
                    "A Merchant Center Account ID. Only required if uploading with item " +
                    "attributes.",
                Default = null)]
            public long? MerchantCenterAccountId { get; set; }

            /// <summary>
            /// A two-letter country code of the location associated with the feed where your items
            /// are uploaded. Only required if uploading with item attributes.
            /// For a list of country codes see:
            /// https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16
            /// </summary>
            [Option("countryCode", Required = false, HelpText =
                    "A two-letter country code of the location associated with the feed where your " +
                    "items are uploaded. Only required if uploading with item attributes.\nFor a " +
                    "list of country codes see: " +
                    "https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16",
                Default = null)]
            public string CountryCode { get; set; }

            /// <summary>
            /// A two-letter language code of the language associated with the feed where your items
            /// are uploaded. Only required if uploading with item attributes. For a list of
            /// language codes see:
            /// https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7
            /// </summary>
            [Option("languageCode", Required = false, HelpText =
                    "A two-letter language code of the language associated with the feed where " +
                    "your items are uploaded. Only required if uploading with item attributes.\n" +
                    "For a list of language codes see: " +
                    "https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7",
                Default = null)]
            public string LanguageCode { get; set; }

            /// <summary>
            /// The number of items sold. Can only be set when at least one other item attribute has
            /// been provided. Only required if uploading with item attributes.
            /// </summary>
            [Option("quantity", Required = false, HelpText =
                    "The number of items sold. Only required if uploading with item attributes.",
                Default = 1)]
            public long Quantity { get; set; }

            /// <summary>
            ///  The consent status for ad personalization.
            /// </summary>
            [Option("adPersonalizationConsent", Required = false, HelpText =
                "The consent status for ad user data.")]
            public ConsentStatus? AdPersonalizationConsent { get; set; }

            /// <summary>
            ///  The consent status for ad user data.
            /// </summary>
            [Option("adUserDataConsent", Required = false, HelpText =
                "The consent status for ad user data.")]
            public ConsentStatus? AdUserDataConsent { 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);

            UploadStoreSalesTransactions codeExample = new UploadStoreSalesTransactions();
            Console.WriteLine(codeExample.Description);
            codeExample.Run(new GoogleAdsClient(), options.CustomerId, options.ConversionActionId,
                options.OfflineUserDataJobType, options.ExternalId,
                options.AdvertiserUploadDateTime, options.BridgeMapVersionId, options.PartnerId,
                options.CustomKey, options.ItemId, options.MerchantCenterAccountId,
                options.CountryCode,
                options.LanguageCode, options.Quantity, options.AdPersonalizationConsent,
                options.AdUserDataConsent);
        }

        // Gets a digest for generating hashed values using SHA-256. You must normalize and hash the
        // the value for any field where the name begins with "hashed". See the normalizeAndHash()
        // method.
        private static readonly SHA256 _digest = SHA256.Create();

        // If uploading data with custom key and values, specify the value:
        private const string CUSTOM_VALUE = "INSERT_CUSTOM_VALUE_HERE";

        /// <summary>
        /// Returns a description about the code example.
        /// </summary>
        public override string Description =>
            "This code example uploads offline data for store sales transactions. This feature " +
            "is only available to allowlisted accounts. See " +
            "https://support.google.com/google-ads/answer/7620302 for more details.";

        /// <summary>
        /// Runs the code example.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="offlineUserDataJobType">The type of user data in the job (first or third
        /// party). If you have an official store sales partnership with Google, use
        /// StoreSalesUploadThirdParty. Otherwise, use StoreSalesUploadFirstParty or
        /// omit this parameter.</param>
        /// <param name="externalId">Optional (but recommended) external ID to identify the offline
        /// user data job.</param>
        /// <param name="advertiserUploadDateTime">Date and time the advertiser uploaded data to the
        /// partner. Only required if uploading third party data.</param>
        /// <param name="bridgeMapVersionId">Version of partner IDs to be used for uploads. Only
        /// required if uploading third party data.</param>
        /// <param name="partnerId">ID of the third party partner. Only required if uploading third
        /// party data.</param>
        /// <param name="customKey">Optional custom key name. Only required if uploading data
        /// with custom key and values.</param>
        /// <param name="itemId">A unique identifier of a product, either the Merchant Center Item
        /// ID or Global Trade Item Number (GTIN). Only required if uploading with item
        /// attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID. Only required if uploading with
        /// item attributes.</param>
        /// <param name="countryCode">A two-letter country code of the location associated with the
        /// feed where your items are uploaded. Only required if uploading with item
        /// attributes.</param>
        /// <param name="languageCode">A two-letter language code of the language associated with
        /// the feed where your items are uploaded. Only required if uploading with item
        /// attributes.</param>
        /// <param name="quantity">The number of items sold. Only required if uploading with item
        /// attributes.</param>
        /// <param name="adPersonalizationConsent">The consent status for ad personalization.
        /// </param>
        /// <param name="adUserDataConsent">The consent status for ad user data.</param>
        public void Run(GoogleAdsClient client, long customerId, long conversionActionId,
            OfflineUserDataJobType offlineUserDataJobType, long? externalId,
            string advertiserUploadDateTime, string bridgeMapVersionId, long? partnerId,
            string customKey, string itemId, long? merchantCenterAccountId, string countryCode,
            string languageCode, long quantity, ConsentStatus? adPersonalizationConsent,
            ConsentStatus? adUserDataConsent)
        {
            // Get the OfflineUserDataJobServiceClient.
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient =
                client.GetService(Services.V17.OfflineUserDataJobService);

            // Ensure that a valid job type is provided.
            if (offlineUserDataJobType != OfflineUserDataJobType.StoreSalesUploadFirstParty &
                offlineUserDataJobType != OfflineUserDataJobType.StoreSalesUploadThirdParty)
            {
                Console.WriteLine("Invalid job type specified, defaulting to First Party.");
                offlineUserDataJobType = OfflineUserDataJobType.StoreSalesUploadFirstParty;
            }

            try
            {
                // Creates an offline user data job for uploading transactions.
                string offlineUserDataJobResourceName =
                    CreateOfflineUserDataJob(offlineUserDataJobServiceClient, customerId,
                        offlineUserDataJobType, externalId, advertiserUploadDateTime,
                        bridgeMapVersionId, partnerId, customKey);

                // Adds transactions to the job.
                AddTransactionsToOfflineUserDataJob(offlineUserDataJobServiceClient, customerId,
                    offlineUserDataJobResourceName, conversionActionId, customKey, itemId,
                    merchantCenterAccountId, countryCode, languageCode, quantity,
                    adPersonalizationConsent, adUserDataConsent);

                // Issues an asynchronous request to run the offline user data job.
                offlineUserDataJobServiceClient.RunOfflineUserDataJobAsync(
                    offlineUserDataJobResourceName);

                Console.WriteLine("Sent request to asynchronously run offline user data job " +
                    $"{offlineUserDataJobResourceName}.");

                // Offline user data jobs may take up to 24 hours to complete, so instead of waiting
                // for the job to complete, retrieves and displays the job status once and then
                // prints the query to use to check the job again later.
                CheckJobStatus(client, customerId, offlineUserDataJobResourceName);
            }
            catch (GoogleAdsException e)
            {
                Console.WriteLine("Failure:");
                Console.WriteLine($"Message: {e.Message}");
                Console.WriteLine($"Failure: {e.Failure}");
                Console.WriteLine($"Request ID: {e.RequestId}");
                throw;
            }
        }

        /// <summary>
        /// Creates an offline user data job for uploading store sales transactions.
        /// </summary>
        /// <param name="offlineUserDataJobServiceClient">The offline user data job service
        /// client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobType">The type of user data in the job (first or third
        /// party). If you have an official store sales partnership with Google, use
        /// StoreSalesUploadThirdParty. Otherwise, use StoreSalesUploadFirstParty or
        /// omit this parameter.</param>
        /// <param name="externalId">Optional (but recommended) external ID to identify the offline
        /// user data job.</param>
        /// <param name="advertiserUploadDateTime">Date and time the advertiser uploaded data to the
        /// partner. Only required if uploading third party data.</param>
        /// <param name="bridgeMapVersionId">Version of partner IDs to be used for uploads. Only
        /// required if uploading third party data.</param>
        /// <param name="partnerId">ID of the third party partner. Only required if uploading third
        /// party data.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        /// and value.</param>
        /// <returns>The resource name of the created job.</returns>
        private string CreateOfflineUserDataJob(
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId,
            OfflineUserDataJobType offlineUserDataJobType, long? externalId,
            string advertiserUploadDateTime, string bridgeMapVersionId, long? partnerId,
            string customKey)
        {
            // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses
            // the term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is
            // called loyaltyFraction in the Google Ads API.

            // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
            // details.
            StoreSalesMetadata storeSalesMetadata = new StoreSalesMetadata()
            {
                // Sets the fraction of your overall sales that you (or the advertiser, in the third
                // party case) can associate with a customer (email, phone number, address, etc.) in
                // your database or loyalty program.
                // For example, set this to 0.7 if you have 100 transactions over 30 days, and out
                // of those 100 transactions, you can identify 70 by an email address or phone
                // number.
                LoyaltyFraction = 0.7,
                // Sets the fraction of sales you're uploading out of the overall sales that you (or
                // the advertiser, in the third party case) can associate with a customer. In most
                // cases, you will set this to 1.0.
                // Continuing the example above for loyalty fraction, a value of 1.0 here indicates
                // that you are uploading all 70 of the transactions that can be identified by an
                // email address or phone number.
                TransactionUploadFraction = 1.0
            };

            // Apply the custom key if provided.
            if (!string.IsNullOrEmpty(customKey))
            {
                storeSalesMetadata.CustomKey = customKey;
            }

            // Creates additional metadata required for uploading third party data.
            if (offlineUserDataJobType == OfflineUserDataJobType.StoreSalesUploadThirdParty)
            {
                StoreSalesThirdPartyMetadata storeSalesThirdPartyMetadata =
                    new StoreSalesThirdPartyMetadata()
                    {
                        // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                        AdvertiserUploadDateTime = advertiserUploadDateTime,

                        // Sets the fraction of transactions you received from the advertiser that
                        // have valid formatting and values. This captures any transactions the
                        // advertiser provided to you but which you are unable to upload to Google
                        // due to formatting errors or missing data.
                        // In most cases, you will set this to 1.0.
                        ValidTransactionFraction = 1.0,

                        // Sets the fraction of valid transactions (as defined above) you received
                        // from the advertiser that you (the third party) have matched to an
                        // external user ID on your side.
                        // In most cases, you will set this to 1.0.
                        PartnerMatchFraction = 1.0,

                        // Sets the fraction of transactions you (the third party) are uploading out
                        // of the transactions you received from the advertiser that meet both of
                        // the following criteria:
                        // 1. Are valid in terms of formatting and values. See valid transaction
                        // fraction above.
                        // 2. You matched to an external user ID on your side. See partner match
                        // fraction above.
                        // In most cases, you will set this to 1.0.
                        PartnerUploadFraction = 1.0,

                        // Sets the version of partner IDs to be used for uploads.
                        // Please speak with your Google representative to get the values to use for
                        // the bridge map version and partner IDs.
                        BridgeMapVersionId = bridgeMapVersionId,
                    };

                // Sets the third party partner ID uploading the transactions.
                if (partnerId.HasValue)
                {
                    storeSalesThirdPartyMetadata.PartnerId = partnerId.Value;
                }

                storeSalesMetadata.ThirdPartyMetadata = storeSalesThirdPartyMetadata;
            }

            // Creates a new offline user data job.
            OfflineUserDataJob offlineUserDataJob = new OfflineUserDataJob()
            {
                Type = offlineUserDataJobType,
                StoreSalesMetadata = storeSalesMetadata
            };

            if (externalId.HasValue)
            {
                offlineUserDataJob.ExternalId = externalId.Value;
            }

            // Issues a request to create the offline user data job.
            CreateOfflineUserDataJobResponse createOfflineUserDataJobResponse =
                offlineUserDataJobServiceClient.CreateOfflineUserDataJob(
                    customerId.ToString(), offlineUserDataJob);
            string offlineUserDataJobResourceName = createOfflineUserDataJobResponse.ResourceName;
            Console.WriteLine("Created an offline user data job with resource name: " +
                $"{offlineUserDataJobResourceName}.");
            return offlineUserDataJobResourceName;
        }

        /// <summary>
        /// Adds operations to a job for a set of sample transactions.
        /// </summary>
        /// <param name="offlineUserDataJobServiceClient">The offline user data job service
        /// client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobResourceName">The resource name of the job to which to
        /// add transactions.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        /// and value.</param>
        /// <param name="itemId">A unique identifier of a product, or null if not uploading with
        /// item attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID, or null if not
        /// uploading with item attributes.</param>
        /// <param name="countryCode">A two-letter country code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="languageCode">A two-letter language code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="quantity">The number of items sold, or null if not uploading with
        /// item attributes.</param>
        /// <param name="adPersonalizationConsent">The consent status for ad personalization.
        /// </param>
        /// <param name="adUserDataConsent">The consent status for ad user data.</param>
        private void AddTransactionsToOfflineUserDataJob(
            OfflineUserDataJobServiceClient offlineUserDataJobServiceClient, long customerId,
            string offlineUserDataJobResourceName, long conversionActionId, string customKey,
            string itemId, long? merchantCenterAccountId, string countryCode, string languageCode,
            long quantity, ConsentStatus? adPersonalizationConsent,
            ConsentStatus? adUserDataConsent)
        {
            // Constructions an operation for each transaction.
            List<OfflineUserDataJobOperation> userDataJobOperations =
                BuildOfflineUserDataJobOperations(customerId, conversionActionId, customKey, itemId,
                    merchantCenterAccountId, countryCode, languageCode, quantity,
                    adPersonalizationConsent, adUserDataConsent);

            // Constructs a request with partial failure enabled to add the operations to the
            // offline user data job, and enable_warnings set to true to retrieve warnings.
            AddOfflineUserDataJobOperationsRequest request =
                new AddOfflineUserDataJobOperationsRequest()
            {
                EnablePartialFailure = true,
                ResourceName = offlineUserDataJobResourceName,
                Operations = { userDataJobOperations },
                EnableWarnings = true,
            };

            AddOfflineUserDataJobOperationsResponse response = offlineUserDataJobServiceClient
                .AddOfflineUserDataJobOperations(request);

            // Prints the status message if any partial failure error is returned.
            // NOTE: The details of each partial failure error are not printed here, you can refer
            // to the example HandlePartialFailure.cs to learn more.
            if (response.PartialFailureError != null)
            {
                Console.WriteLine($"Encountered {response.PartialFailureError.Details.Count} " +
                    $"partial failure errors while adding {userDataJobOperations.Count} " +
                    "operations to the offline user data job: " +
                    $"'{response.PartialFailureError.Message}'. Only the successfully added " +
                    "operations will be executed when the job runs.");
            }
            else
            {
                Console.WriteLine($"Successfully added {userDataJobOperations.Count} operations " +
                    "to the offline user data job.");
            }

            // Prints the number of warnings if any warnings are returned. You can access
            // details of each warning using the same approach you'd use for partial failure
            // errors.
            if (request.EnableWarnings && response.Warnings != null)
            {
                // Extracts the warnings from the response.
                GoogleAdsFailure warnings = response.Warnings;
                Console.WriteLine($"{warnings.Errors.Count} warning(s) occurred");
            }
        }

        /// <summary>
        /// Creates a list of offline user data job operations for sample transactions.
        /// </summary>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="conversionActionId">The ID of a store sales conversion action.</param>
        /// <param name="customKey">The custom key, or null if not uploading data with custom key
        /// and value.</param>
        /// <param name="itemId">A unique identifier of a product, or null if not uploading with
        /// item attributes.</param>
        /// <param name="merchantCenterAccountId">A Merchant Center Account ID, or null if not
        /// uploading with item attributes.</param>
        /// <param name="countryCode">A two-letter country code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="languageCode">A two-letter language code, or null if not uploading with
        /// item attributes.</param>
        /// <param name="quantity">The number of items sold, or null if not uploading with
        /// item attributes.</param>
        /// <param name="adPersonalizationConsent">The consent status for ad personalization.
        /// </param>
        /// <param name="adUserDataConsent">The consent status for ad user data.</param>
        /// <returns>A list of operations.</returns>
        private List<OfflineUserDataJobOperation> BuildOfflineUserDataJobOperations(long customerId,
            long conversionActionId, string customKey, string itemId, long? merchantCenterAccountId,
            string countryCode, string languageCode, long quantity,
            ConsentStatus? adPersonalizationConsent, ConsentStatus? adUserDataConsent)
        {
            // Create the first transaction for upload based on an email address and state.
            UserData userDataWithEmailAddress = new UserData()
            {
                UserIdentifiers =
                {
                    new UserIdentifier()
                    {
                        // Email addresses must be normalized and hashed.
                        HashedEmail = NormalizeAndHash("dana@example.com")
                    },
                    new UserIdentifier()
                    {
                        AddressInfo = new OfflineUserAddressInfo()
                        {
                            State = "NY"
                        }
                    },
                },
                TransactionAttribute = new TransactionAttribute()
                {
                    ConversionAction =
                        ResourceNames.ConversionAction(customerId, conversionActionId),
                    CurrencyCode = "USD",
                    // Converts the transaction amount from $200 USD to micros.
                    // If item attributes are provided, this value represents the total value of the
                    // items after multiplying the unit price per item by the quantity provided in
                    // the ItemAttribute.
                    TransactionAmountMicros = 200L * 1_000_000L,
                    // Specifies the date and time of the transaction. The format is
                    // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                    // timezone offset from UTC. If the offset is absent, the API will
                    // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                    // or "2018-02-01 14:34:30+03:00".
                    TransactionDateTime =
                        DateTime.Today.AddDays(-2).ToString("yyyy-MM-dd HH:mm:ss")
                }
            };

            // Set the custom value if a custom key was provided.
            if (!string.IsNullOrEmpty(customKey))
            {
                userDataWithEmailAddress.TransactionAttribute.CustomValue = CUSTOM_VALUE;
            }

            if (adUserDataConsent != null || adPersonalizationConsent != null)
            {
                // Specifies whether user consent was obtained for the data you are uploading. See
                // https://www.google.com/about/company/user-consent-policy
                // for details.
                userDataWithEmailAddress.Consent = new Consent();

                if (adPersonalizationConsent != null)
                {
                    userDataWithEmailAddress.Consent.AdPersonalization =
                        (ConsentStatus)adPersonalizationConsent;
                }

                if (adUserDataConsent != null)
                {
                    userDataWithEmailAddress.Consent.AdUserData = (ConsentStatus)adUserDataConsent;
                }
            }

            // Creates the second transaction for upload based on a physical address.
            UserData userDataWithPhysicalAddress = new UserData()
            {
                UserIdentifiers =
                {
                    new UserIdentifier()
                    {
                        AddressInfo = new OfflineUserAddressInfo()
                        {
                            // Names must be normalized and hashed.
                            HashedFirstName = NormalizeAndHash("Alex"),
                            HashedLastName = NormalizeAndHash("Quinn"),
                            CountryCode = "US",
                            PostalCode = "10011"
                        }
                    }
                },
                TransactionAttribute = new TransactionAttribute()
                {
                    ConversionAction =
                        ResourceNames.ConversionAction(customerId, conversionActionId),
                    CurrencyCode = "EUR",
                    // Converts the transaction amount from 450 EUR to micros.
                    TransactionAmountMicros = 450L * 1_000_000L,
                    // Specifies the date and time of the transaction. This date and time will be
                    // interpreted by the API using the Google Ads customer's time zone.
                    // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                    // e.g. "2020-05-14 19:07:02".
                    TransactionDateTime = DateTime.Today.AddDays(-1).ToString("yyyy-MM-dd HH:mm:ss")
                }
            };

            // Set the item attribute if provided.
            if (!string.IsNullOrEmpty(itemId))
            {
                userDataWithPhysicalAddress.TransactionAttribute.ItemAttribute = new ItemAttribute
                {
                    ItemId = itemId,
                    MerchantId = merchantCenterAccountId.Value,
                    CountryCode = countryCode,
                    LanguageCode = languageCode,
                    // Quantity field should only be set when at least one of the other item
                    // attributes is present.
                    Quantity = quantity
                };
            }

            // Specifies whether user consent was obtained for the data you are uploading. See
            // https://www.google.com/about/company/user-consent-policy
            // for details.
            userDataWithPhysicalAddress.Consent = new Consent()
            {
                AdPersonalization = ConsentStatus.Granted,
                AdUserData = ConsentStatus.Denied
            };


            // Creates the operations to add the two transactions.
            List<OfflineUserDataJobOperation> operations = new List<OfflineUserDataJobOperation>()
            {
                new OfflineUserDataJobOperation()
                {
                    Create = userDataWithEmailAddress
                },
                new OfflineUserDataJobOperation()
                {
                    Create = userDataWithPhysicalAddress
                }
            };

            return operations;
        }

        /// <summary>
        /// Normalizes and hashes a string value.
        /// </summary>
        /// <param name="value">The value to normalize and hash.</param>
        /// <returns>The normalized and hashed value.</returns>
        private static string NormalizeAndHash(string value)
        {
            return ToSha256String(_digest, ToNormalizedValue(value));
        }

        /// <summary>
        /// Hash a string value using SHA-256 hashing algorithm.
        /// </summary>
        /// <param name="digest">Provides the algorithm for SHA-256.</param>
        /// <param name="value">The string value (e.g. an email address) to hash.</param>
        /// <returns>The hashed value.</returns>
        private static string ToSha256String(SHA256 digest, string value)
        {
            byte[] digestBytes = digest.ComputeHash(Encoding.UTF8.GetBytes(value));
            // Convert the byte array into an unhyphenated hexadecimal string.
            return BitConverter.ToString(digestBytes).Replace("-", string.Empty);
        }

        /// <summary>
        /// Removes leading and trailing whitespace and converts all characters to
        /// lower case.
        /// </summary>
        /// <param name="value">The value to normalize.</param>
        /// <returns>The normalized value.</returns>
        private static string ToNormalizedValue(string value)
        {
            return value.Trim().ToLower();
        }

        /// <summary>
        /// Retrieves, checks, and prints the status of the offline user data job.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID for which the call is made.</param>
        /// <param name="offlineUserDataJobResourceName">The resource name of the job whose status
        /// you wish to check.</param>
        private void CheckJobStatus(GoogleAdsClient client, long customerId,
            string offlineUserDataJobResourceName)
        {
            GoogleAdsServiceClient googleAdsServiceClient =
                client.GetService(Services.V17.GoogleAdsService);

            string query = $@"SELECT offline_user_data_job.resource_name,
                    offline_user_data_job.id,
                    offline_user_data_job.status,
                    offline_user_data_job.type,
                    offline_user_data_job.failure_reason
                FROM offline_user_data_job
                WHERE offline_user_data_job.resource_name = '{offlineUserDataJobResourceName}'";

            // Issues the query and gets the GoogleAdsRow containing the job from the response.
            GoogleAdsRow googleAdsRow = googleAdsServiceClient.Search(
                customerId.ToString(), query).First();

            OfflineUserDataJob offlineUserDataJob = googleAdsRow.OfflineUserDataJob;

            OfflineUserDataJobStatus jobStatus = offlineUserDataJob.Status;
            Console.WriteLine($"Offline user data job ID {offlineUserDataJob.Id} with type " +
                $"'{offlineUserDataJob.Type}' has status {offlineUserDataJob.Status}.");

            if (jobStatus == OfflineUserDataJobStatus.Failed)
            {
                Console.WriteLine($"\tFailure reason: {offlineUserDataJob.FailureReason}");
            }
            else if (jobStatus == OfflineUserDataJobStatus.Pending |
                jobStatus == OfflineUserDataJobStatus.Running)
            {
                Console.WriteLine("\nTo check the status of the job periodically, use the" +
                    $"following GAQL query with GoogleAdsService.Search:\n{query}\n");
            }
        }
    }
}

      

PHP

<?php

/**
 * Copyright 2020 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\Remarketing;

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\V17\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V17\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\V17\GoogleAdsException;
use Google\Ads\GoogleAds\Lib\V17\GoogleAdsServerStreamDecorator;
use Google\Ads\GoogleAds\Util\V17\GoogleAdsFailures;
use Google\Ads\GoogleAds\Util\V17\ResourceNames;
use Google\Ads\GoogleAds\V17\Common\Consent;
use Google\Ads\GoogleAds\V17\Common\ItemAttribute;
use Google\Ads\GoogleAds\V17\Common\OfflineUserAddressInfo;
use Google\Ads\GoogleAds\V17\Common\StoreSalesMetadata;
use Google\Ads\GoogleAds\V17\Common\StoreSalesThirdPartyMetadata;
use Google\Ads\GoogleAds\V17\Common\TransactionAttribute;
use Google\Ads\GoogleAds\V17\Common\UserData;
use Google\Ads\GoogleAds\V17\Common\UserIdentifier;
use Google\Ads\GoogleAds\V17\Enums\ConsentStatusEnum\ConsentStatus;
use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobFailureReasonEnum\OfflineUserDataJobFailureReason;
use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobStatusEnum\OfflineUserDataJobStatus;
use Google\Ads\GoogleAds\V17\Enums\OfflineUserDataJobTypeEnum\OfflineUserDataJobType;
use Google\Ads\GoogleAds\V17\Errors\GoogleAdsError;
use Google\Ads\GoogleAds\V17\Resources\OfflineUserDataJob;
use Google\Ads\GoogleAds\V17\Services\AddOfflineUserDataJobOperationsRequest;
use Google\Ads\GoogleAds\V17\Services\AddOfflineUserDataJobOperationsResponse;
use Google\Ads\GoogleAds\V17\Services\Client\OfflineUserDataJobServiceClient;
use Google\Ads\GoogleAds\V17\Services\CreateOfflineUserDataJobRequest;
use Google\Ads\GoogleAds\V17\Services\CreateOfflineUserDataJobResponse;
use Google\Ads\GoogleAds\V17\Services\GoogleAdsRow;
use Google\Ads\GoogleAds\V17\Services\OfflineUserDataJobOperation;
use Google\Ads\GoogleAds\V17\Services\RunOfflineUserDataJobRequest;
use Google\Ads\GoogleAds\V17\Services\SearchGoogleAdsStreamRequest;
use Google\ApiCore\ApiException;

/**
 * Uploads offline data for store sales transactions.
 *
 * This feature is only available to allowlisted accounts. See
 * https://support.google.com/google-ads/answer/7620302 for more details.
 */
class UploadStoreSalesTransactions
{
    private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE';

    /**
     * The type of user data in the job (first or third party). If you have an official
     * store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY.
     * Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.
     */
    private const OFFLINE_USER_DATA_JOB_TYPE = 'STORE_SALES_UPLOAD_FIRST_PARTY';
    /** The ID of a store sales conversion action. */
    private const CONVERSION_ACTION_ID = 'INSERT_CONVERSION_ACTION_ID_HERE';
    /**
     * Optional (but recommended) external ID to identify the offline user data job.
     * The external ID for the offline user data job.
     */
    private const EXTERNAL_ID = null;
    /**
     * Only required after creating a custom key and custom values in the account.
     * Custom key and values are used to segment store sales conversions.
     * This measurement can be used to provide more advanced insights.
     */
    private const CUSTOM_KEY = null;

    // Optional: If uploading third party data, also specify the following values:
    /**
     * The date and time the advertiser uploaded data to the partner.
     * The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
     */
    private const ADVERTISER_UPLOAD_DATE_TIME = null;
    /** The version of partner IDs to be used for uploads. */
    private const BRIDGE_MAP_VERSION_ID = null;
    /** The ID of the third party partner. */
    private const PARTNER_ID = null;
    // Optional: The consent status for ad personalization.
    private const AD_PERSONALIZATION_CONSENT = null;
    // Optional: The consent status for ad user data.
    private const AD_USER_DATA_CONSENT = null;

    // Optional: Below constants are only required if uploading with item attributes.
    /**
     * Specify a unique identifier of a product, either the Merchant Center
     * Item ID or Global Trade Item Number (GTIN).
     */
    private const ITEM_ID = null;
    /**
     * Specify a Merchant Center Account ID.
     */
    private const MERCHANT_CENTER_ACCOUNT_ID = null;
    /**
     * Specify a two-letter country code of the location associated with the
     * feed where your items are uploaded.
     * For a list of country codes see:
     * https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-16
     */
    private const COUNTRY_CODE = null;
    /**
     * Specify a two-letter language code of the language associated with
     * the feed where your items are uploaded.
     * For a list of language codes see:
     * https://developers.google.com/google-ads/api/reference/data/codes-formats#expandable-7
     */
    private const LANGUAGE_CODE = null;
    /**
     * Specify a number of items sold.
     */
    private const QUANTITY = 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::OFFLINE_USER_DATA_JOB_TYPE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::CONVERSION_ACTION_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::AD_PERSONALIZATION_CONSENT => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::AD_USER_DATA_CONSENT => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::EXTERNAL_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::CUSTOM_KEY => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::ADVERTISER_UPLOAD_DATE_TIME => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::BRIDGE_MAP_VERSION_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::PARTNER_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::ITEM_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::MERCHANT_CENTER_ACCOUNT_ID => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::COUNTRY_CODE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::LANGUAGE_CODE => GetOpt::OPTIONAL_ARGUMENT,
            ArgumentNames::QUANTITY => GetOpt::OPTIONAL_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::OFFLINE_USER_DATA_JOB_TYPE]
                    ?: self::OFFLINE_USER_DATA_JOB_TYPE,
                $options[ArgumentNames::CONVERSION_ACTION_ID] ?: self::CONVERSION_ACTION_ID,
                $options[ArgumentNames::AD_PERSONALIZATION_CONSENT]
                    ? ConsentStatus::value($options[ArgumentNames::AD_PERSONALIZATION_CONSENT])
                    : self::AD_PERSONALIZATION_CONSENT,
                $options[ArgumentNames::AD_USER_DATA_CONSENT]
                    ? ConsentStatus::value($options[ArgumentNames::AD_USER_DATA_CONSENT])
                    : self::AD_USER_DATA_CONSENT,
                $options[ArgumentNames::EXTERNAL_ID] ?: self::EXTERNAL_ID,
                $options[ArgumentNames::CUSTOM_KEY] ?: self::CUSTOM_KEY,
                $options[ArgumentNames::ADVERTISER_UPLOAD_DATE_TIME]
                    ?: self::ADVERTISER_UPLOAD_DATE_TIME,
                $options[ArgumentNames::BRIDGE_MAP_VERSION_ID] ?: self::BRIDGE_MAP_VERSION_ID,
                $options[ArgumentNames::PARTNER_ID] ?: self::PARTNER_ID,
                $options[ArgumentNames::ITEM_ID] ?: self::ITEM_ID,
                $options[ArgumentNames::MERCHANT_CENTER_ACCOUNT_ID]
                    ?: self::MERCHANT_CENTER_ACCOUNT_ID,
                $options[ArgumentNames::COUNTRY_CODE] ?: self::COUNTRY_CODE,
                $options[ArgumentNames::LANGUAGE_CODE] ?: self::LANGUAGE_CODE,
                $options[ArgumentNames::QUANTITY] ?: self::QUANTITY
            );
        } 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|null $offlineUserDataJobType the type of offline user data in the job (first
     *     party or third party). If you have an official store sales partnership with Google, use
     *     `STORE_SALES_UPLOAD_THIRD_PARTY`. Otherwise, use `STORE_SALES_UPLOAD_FIRST_PARTY`
     * @param int $conversionActionId the ID of a store sales conversion action
     * @param int|null $adPersonalizationConsent the ad personalization consent status
     * @param int|null $adUserDataConsent the ad user data consent status
     * @param int|null $externalId optional (but recommended) external ID for the offline user data
     *     job
     * @param string|null $customKey the custom key to segment store sales conversions. Only
     *     required after creating a custom key and custom values in the account.
     * @param string|null $advertiserUploadDateTime date and time the advertiser uploaded data to
     *     the partner. Only required for third party uploads
     * @param string|null $bridgeMapVersionId version of partner IDs to be used for uploads. Only
     *     required for third party uploads
     * @param int|null $partnerId ID of the third party partner. Only required for third party
     *     uploads
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    public static function runExample(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        ?string $offlineUserDataJobType,
        int $conversionActionId,
        ?int $adPersonalizationConsent,
        ?int $adUserDataConsent,
        ?int $externalId,
        ?string $customKey,
        ?string $advertiserUploadDateTime,
        ?string $bridgeMapVersionId,
        ?int $partnerId,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ) {
        $offlineUserDataJobServiceClient = $googleAdsClient->getOfflineUserDataJobServiceClient();

        // Creates an offline user data job for uploading transactions.
        $offlineUserDataJobResourceName = self::createOfflineUserDataJob(
            $offlineUserDataJobServiceClient,
            $customerId,
            $offlineUserDataJobType,
            $externalId,
            $customKey,
            $advertiserUploadDateTime,
            $bridgeMapVersionId,
            $partnerId
        );

        // Adds transactions to the job.
        self::addTransactionsToOfflineUserDataJob(
            $offlineUserDataJobServiceClient,
            $customerId,
            $offlineUserDataJobResourceName,
            $conversionActionId,
            $adPersonalizationConsent,
            $adUserDataConsent,
            $itemId,
            $merchantCenterAccountId,
            $countryCode,
            $languageCode,
            $quantity
        );

        // Issues an asynchronous request to run the offline user data job.
        $offlineUserDataJobServiceClient->runOfflineUserDataJob(
            RunOfflineUserDataJobRequest::build($offlineUserDataJobResourceName)
        );

        printf(
            "Sent request to asynchronously run offline user data job: '%s'.%s",
            $offlineUserDataJobResourceName,
            PHP_EOL
        );

        // Offline user data jobs may take up to 24 hours to complete, so instead of waiting for the
        // job to complete, retrieves and displays the job status once and then prints the query to
        // use to check the job again later.
        self::checkJobStatus($googleAdsClient, $customerId, $offlineUserDataJobResourceName);
    }

    /**
     * Creates an offline user data job for uploading store sales transactions.
     *
     * @param OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient the offline user
     *     data job service client
     * @param int $customerId the customer ID
     * @param string|null $offlineUserDataJobType the type of offline user data in the job (first
     *     party or third party). If you have an official store sales partnership with Google, use
     *     `STORE_SALES_UPLOAD_THIRD_PARTY`. Otherwise, use `STORE_SALES_UPLOAD_FIRST_PARTY`
     * @param int|null $externalId optional (but recommended) external ID for the offline user data
     *     job
     * @param string|null $customKey the custom key to segment store sales conversions. Only
     *     required after creating a custom key and custom values in the account.
     * @param string|null $advertiserUploadDateTime date and time the advertiser uploaded data to
     *     the partner. Only required for third party uploads
     * @param string|null $bridgeMapVersionId version of partner IDs to be used for uploads. Only
     *     required for third party uploads
     * @param int|null $partnerId ID of the third party partner. Only required for third party
     *     uploads
     * @return string the resource name of the created job
     */
    private static function createOfflineUserDataJob(
        OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient,
        int $customerId,
        ?string $offlineUserDataJobType,
        ?int $externalId,
        ?string $customKey,
        ?string $advertiserUploadDateTime,
        ?string $bridgeMapVersionId,
        ?int $partnerId
    ): string {
        // TIP: If you are migrating from the AdWords API, please note that Google Ads API uses the
        // term "fraction" instead of "rate". For example, loyaltyRate in the AdWords API is called
        // loyaltyFraction in the Google Ads API.
        // Please refer to https://support.google.com/google-ads/answer/7506124 for additional
        // details.
        $storeSalesMetadata = new StoreSalesMetadata([
            // Sets the fraction of your overall sales that you (or the advertiser, in the third
            // party case) can associate with a customer (email, phone number, address, etc.) in
            // your database or loyalty program.
            // For example, set this to 0.7 if you have 100 transactions over 30 days, and out of
            // those 100 transactions, you can identify 70 by an email address or phone number.
            'loyalty_fraction' => 0.7,
            // Sets the fraction of sales you're uploading out of the overall sales that you (or the
            // advertiser, in the third party case) can associate with a customer. In most cases,
            // you will set this to 1.0.
            // Continuing the example above for loyalty fraction, a value of 1.0 here indicates that
            // you are uploading all 70 of the transactions that can be identified by an email
            // address or phone number.
            'transaction_upload_fraction' => 1.0,
        ]);
        if (!is_null($customKey)) {
            $storeSalesMetadata->setCustomKey($customKey);
        }
        if (
            OfflineUserDataJobType::value($offlineUserDataJobType)
                === OfflineUserDataJobType::STORE_SALES_UPLOAD_THIRD_PARTY
        ) {
            // Creates additional metadata required for uploading third party data.
            $storeSalesThirdPartyMetadata = new StoreSalesThirdPartyMetadata([
                // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                'advertiser_upload_date_time' => $advertiserUploadDateTime,
                // Sets the fraction of transactions you received from the advertiser that have
                // valid formatting and values. This captures any transactions the advertiser
                // provided to you but which you are unable to upload to Google due to formatting
                // errors or missing data.
                // In most cases, you will set this to 1.0.
                'valid_transaction_fraction' => 1.0,
                // Sets the fraction of valid transactions (as defined above) you received from the
                // advertiser that you (the third party) have matched to an external user ID on your
                // side.
                // In most cases, you will set this to 1.0.
                'partner_match_fraction' => 1.0,
                // Sets the fraction of transactions you (the third party) are uploading out of the
                // transactions you received from the advertiser that meet both of the following
                // criteria:
                // 1. Are valid in terms of formatting and values. See valid transaction fraction
                // above.
                // 2. You matched to an external user ID on your side. See partner match fraction
                // above.
                // In most cases, you will set this to 1.0.
                'partner_upload_fraction' => 1.0,
                // Please speak with your Google representative to get the values to use for the
                // bridge map version and partner IDs.
                // Sets the version of partner IDs to be used for uploads.
                'bridge_map_version_id' => $bridgeMapVersionId,
                // Sets the third party partner ID uploading the transactions.
                'partner_id' => $partnerId,
            ]);
            $storeSalesMetadata->setThirdPartyMetadata($storeSalesThirdPartyMetadata);
        }
        // Creates a new offline user data job.
        $offlineUserDataJob = new OfflineUserDataJob([
            'type' => OfflineUserDataJobType::value($offlineUserDataJobType),
            'store_sales_metadata' => $storeSalesMetadata
        ]);
        if (!is_null($externalId)) {
            $offlineUserDataJob->setExternalId($externalId);
        }

        // Issues a request to create the offline user data job.
        /** @var CreateOfflineUserDataJobResponse $createOfflineUserDataJobResponse */
        $createOfflineUserDataJobResponse =
            $offlineUserDataJobServiceClient->createOfflineUserDataJob(
                CreateOfflineUserDataJobRequest::build($customerId, $offlineUserDataJob)
            );
        $offlineUserDataJobResourceName = $createOfflineUserDataJobResponse->getResourceName();
        printf(
            "Created an offline user data job with resource name: '%s'.%s",
            $offlineUserDataJobResourceName,
            PHP_EOL
        );

        return $offlineUserDataJobResourceName;
    }

    /**
     * Adds operations to the job for a set of sample transactions.
     *
     * @param OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient the offline user
     *     data job service client
     * @param int $customerId the customer ID
     * @param string $offlineUserDataJobResourceName the resource name of the created offline user
     *     data job
     * @param int $conversionActionId the ID of a store sales conversion action
     * @param int|null $adPersonalizationConsent the ad personalization consent status
     * @param int|null $adUserDataConsent the ad user data consent status
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    private static function addTransactionsToOfflineUserDataJob(
        OfflineUserDataJobServiceClient $offlineUserDataJobServiceClient,
        int $customerId,
        string $offlineUserDataJobResourceName,
        int $conversionActionId,
        ?int $adPersonalizationConsent,
        ?int $adUserDataConsent,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ) {
        // Constructs the operation for each transaction.
        $userDataJobOperations = self::buildOfflineUserDataJobOperations(
            $customerId,
            $conversionActionId,
            $adPersonalizationConsent,
            $adUserDataConsent,
            $itemId,
            $merchantCenterAccountId,
            $countryCode,
            $languageCode,
            $quantity
        );

        // Issues a request to add the operations to the offline user data job.
        /** @var AddOfflineUserDataJobOperationsResponse $operationResponse */
        $request = AddOfflineUserDataJobOperationsRequest::build(
            $offlineUserDataJobResourceName,
            $userDataJobOperations
        );
        // (Optional) Enables partial failure and warnings.
        $request->setEnablePartialFailure(true)->setEnableWarnings(true);
        $response = $offlineUserDataJobServiceClient->addOfflineUserDataJobOperations($request);

        // Prints the status message if any partial failure error is returned.
        // NOTE: The details of each partial failure error are not printed here, you can refer to
        // the example HandlePartialFailure.php to learn more.
        if ($response->hasPartialFailureError()) {
            printf(
                "Encountered %d partial failure errors while adding %d operations to the "
                . "offline user data job: '%s'. Only the successfully added operations will be "
                . "executed when the job runs.%s",
                count($response->getPartialFailureError()->getDetails()),
                count($userDataJobOperations),
                $response->getPartialFailureError()->getMessage(),
                PHP_EOL
            );
        } else {
            printf(
                "Successfully added %d operations to the offline user data job.%s",
                count($userDataJobOperations),
                PHP_EOL
            );
        }

        // Prints the number of warnings if any warnings are returned. You can access
        // details of each warning using the same approach you'd use for partial failure
        // errors.
        if ($response->hasWarning()) {
            // Extracts all the warning errors from the response details into a single
            // GoogleAdsFailure object.
            $warningFailure = GoogleAdsFailures::fromAnys($response->getWarning()->getDetails());
            // Prints some information about the warnings encountered.
            printf(
                "Encountered %d warning(s).%s",
                count($warningFailure->getErrors()),
                PHP_EOL
            );
        }
    }

    /**
     * Creates a list of offline user data job operations for sample transactions.
     *
     * @param int $customerId the customer ID
     * @param int $conversionActionId the ID of a store sales conversion action
     * @param int|null $adPersonalizationConsent the ad personalization consent status
     * @param int|null $adUserDataConsent the ad user data consent status
     * @return OfflineUserDataJobOperation[] an array with the operations
     * @param string|null $itemId a unique identifier of a product, either the Merchant Center Item
     *     ID or Global Trade Item Number (GTIN)
     * @param int|null $merchantCenterAccountId a Merchant Center Account ID
     * @param string|null $countryCode a two-letter country code of the location associated with the
     *     feed where your items are uploaded
     * @param string|null $languageCode a two-letter language code of the language associated with
     *     the feed where your items are uploaded
     * @param int|null $quantity the number of items sold. Can only be set when at least one other
     *     item attribute has been provided
     */
    private static function buildOfflineUserDataJobOperations(
        $customerId,
        $conversionActionId,
        ?int $adPersonalizationConsent,
        ?int $adUserDataConsent,
        ?string $itemId,
        ?int $merchantCenterAccountId,
        ?string $countryCode,
        ?string $languageCode,
        ?int $quantity
    ): array {
        // Creates the first transaction for upload based on an email address and state.
        $userDataWithEmailAddress = new UserData([
            'user_identifiers' => [
                new UserIdentifier([
                    // Email addresses must be normalized and hashed.
                    'hashed_email' => self::normalizeAndHash('dana@example.com')
                ]),
                new UserIdentifier([
                    'address_info' => new OfflineUserAddressInfo(['state' => 'NY'])
                ])
            ],
            'transaction_attribute' => new TransactionAttribute([
                'conversion_action'
                    => ResourceNames::forConversionAction($customerId, $conversionActionId),
                'currency_code' => 'USD',
                // Converts the transaction amount from $200 USD to micros.
                'transaction_amount_micros' => Helper::baseToMicro(200),
                // Specifies the date and time of the transaction. The format is
                // "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional
                // timezone offset from UTC. If the offset is absent, the API will
                // use the account's timezone as default. Examples: "2018-03-05 09:15:00"
                // or "2018-02-01 14:34:30+03:00".
                'transaction_date_time' => '2020-05-01 23:52:12'
                // OPTIONAL: If uploading data with custom key and values, also specify the
                // following value:
                // 'custom_value' => 'INSERT_CUSTOM_VALUE_HERE'
            ])
        ]);

        // Adds consent information if specified.
        if (!empty($adPersonalizationConsent) || !empty($adUserDataConsent)) {
            $consent = new Consent();
            if (!empty($adPersonalizationConsent)) {
                $consent->setAdPersonalization($adPersonalizationConsent);
            }
            if (!empty($adUserDataConsent)) {
                $consent->setAdUserData($adUserDataConsent);
            }
            // Specifies whether user consent was obtained for the data you are uploading. See
            // https://www.google.com/about/company/user-consent-policy for details.
            $userDataWithEmailAddress->setConsent($consent);
        }

        // Creates the second transaction for upload based on a physical address.
        $userDataWithPhysicalAddress = new UserData([
            'user_identifiers' => [
                new UserIdentifier([
                    'address_info' => new OfflineUserAddressInfo([
                        // First and last name must be normalized and hashed.
                        'hashed_first_name' => self::normalizeAndHash('Dana'),
                        'hashed_last_name' => self::normalizeAndHash('Quinn'),
                        // Country code and zip code are sent in plain text.
                        'country_code' => 'US',
                        'postal_code' => '10011'
                    ])
                ])
            ],
            'transaction_attribute' => new TransactionAttribute([
                'conversion_action'
                    => ResourceNames::forConversionAction($customerId, $conversionActionId),
                'currency_code' => 'EUR',
                // Converts the transaction amount from 450 EUR to micros.
                'transaction_amount_micros' => Helper::baseToMicro(450),
                // Specifies the date and time of the transaction. This date and time will be
                // interpreted by the API using the Google Ads customer's time zone.
                // The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
                'transaction_date_time' => '2020-05-14 19:07:02'
            ])
        ]);

        // Optional: If uploading data with item attributes, also assign these values
        // in the transaction attribute.
        if (!empty($itemId)) {
            $userDataWithPhysicalAddress->getTransactionAttribute()->setItemAttribute(
                new ItemAttribute([
                    'item_id' => $itemId,
                    'merchant_id' => $merchantCenterAccountId,
                    'country_code' => $countryCode,
                    'language_code' => $languageCode,
                    // Quantity field should only be set when at least one of the other item
                    // attribute fields is present.
                    'quantity' => $quantity
                ])
            );
        }

        // Creates the operations to add the two transactions.
        $operations = [];
        foreach ([$userDataWithEmailAddress, $userDataWithPhysicalAddress] as $userData) {
            $operations[] = new OfflineUserDataJobOperation(['create' => $userData]);
        }

        return $operations;
    }

    /**
     * Returns the result of normalizing and then hashing the string.
     * Private customer data must be hashed during upload, as described at
     * https://support.google.com/google-ads/answer/7506124.
     *
     * @param string $value the value to normalize and hash
     * @return string the normalized and hashed value
     */
    private static function normalizeAndHash(string $value): string
    {
        return hash('sha256', strtolower(trim($value)));
    }

    /**
     * Retrieves, checks, and prints the status of the offline user data job.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $offlineUserDataJobResourceName the resource name of the created offline user
     *     data job
     */
    private static function checkJobStatus(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $offlineUserDataJobResourceName
    ) {
        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();

        // Creates a query that retrieves the offline user data.
        $query = "SELECT offline_user_data_job.resource_name, "
            . "offline_user_data_job.id, "
            . "offline_user_data_job.status, "
            . "offline_user_data_job.type, "
            . "offline_user_data_job.failure_reason "
            . "FROM offline_user_data_job "
            . "WHERE offline_user_data_job.resource_name = '$offlineUserDataJobResourceName'";

        // Issues a search stream request.
        /** @var GoogleAdsServerStreamDecorator $stream */
        $stream = $googleAdsServiceClient->searchStream(
            SearchGoogleAdsStreamRequest::build($customerId, $query)
        );

        // Prints out some information about the offline user data.
        /** @var GoogleAdsRow $googleAdsRow */
        $googleAdsRow = $stream->iterateAllElements()->current();
        $offlineUserDataJob = $googleAdsRow->getOfflineUserDataJob();
        printf(
            "Offline user data job ID %d with type '%s' has status: %s.%s",
            $offlineUserDataJob->getId(),
            OfflineUserDataJobType::name($offlineUserDataJob->getType()),
            OfflineUserDataJobStatus::name($offlineUserDataJob->getStatus()),
            PHP_EOL
        );

        if (OfflineUserDataJobStatus::FAILED === $offlineUserDataJob->getStatus()) {
            printf(
                "  Failure reason: %s%s",
                OfflineUserDataJobFailureReason::name($offlineUserDataJob->getFailureReason()),
                PHP_EOL
            );
        } elseif (
            OfflineUserDataJobStatus::PENDING === $offlineUserDataJob->getStatus()
            || OfflineUserDataJobStatus::RUNNING === $offlineUserDataJob->getStatus()
        ) {
            printf(
                '%1$sTo check the status of the job periodically, use the following GAQL '
                . 'query with GoogleAdsService.search:%1$s%2$s%1$s.',
                PHP_EOL,
                $query
            );
        }
    }
}

UploadStoreSalesTransactions::main();

      

Python

#!/usr/bin/env python
# Copyright 2020 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 uploads offline conversion data for store sales transactions.

This feature is only available to allowlisted accounts.
See https://support.google.com/google-ads/answer/7620302 for more details.
"""

import argparse
from datetime import datetime
import hashlib
import sys

from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException


def main(
    client,
    customer_id,
    conversion_action_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
    ad_user_data_consent,
    ad_personalization_consent,
):
    """Uploads offline conversion data for store sales transactions.

    Args:
        client: An initialized Google Ads client.
        customer_id: The Google Ads customer ID.
        conversion_action_id: The ID of a store sales conversion action.
        offline_user_data_job_type: Optional type of offline user data in the
            job (first party or third party). If you have an official store
            sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY.
            Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY.
        external_id: Optional, but recommended, external ID for the offline
            user data job.
        advertiser_upload_date_time: Optional date and time the advertiser
            uploaded data to the partner. Only required for third party uploads.
            The format is 'yyyy-mm-dd hh:mm:ss+|-hh:mm', e.g.
            '2019-01-01 12:32:45-08:00'.
        bridge_map_version_id: Optional version of partner IDs to be used for
            uploads. Only required for third party uploads.
        partner_id: Optional ID of the third party partner. Only required for
            third party uploads.
        custom_key: A custom key str to segment store sales conversions. Only
            required after creating a custom key and custom values in the
            account.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.
        ad_user_data_consent: The consent status for ad user data for all
            members in the job.
        ad_personalization_consent: The personalization consent status for ad
            user data for all members in the job.
    """
    # Get the OfflineUserDataJobService and OperationService clients.
    offline_user_data_job_service = client.get_service(
        "OfflineUserDataJobService"
    )

    # Create an offline user data job for uploading transactions.
    offline_user_data_job_resource_name = create_offline_user_data_job(
        client,
        offline_user_data_job_service,
        customer_id,
        offline_user_data_job_type,
        external_id,
        advertiser_upload_date_time,
        bridge_map_version_id,
        partner_id,
        custom_key,
    )

    # Add transactions to the job.
    add_transactions_to_offline_user_data_job(
        client,
        offline_user_data_job_service,
        customer_id,
        offline_user_data_job_resource_name,
        conversion_action_id,
        custom_value,
        item_id,
        merchant_center_account_id,
        country_code,
        language_code,
        quantity,
        ad_user_data_consent,
        ad_personalization_consent,
    )

    # Issue an asynchronous request to run the offline user data job.
    offline_user_data_job_service.run_offline_user_data_job(
        resource_name=offline_user_data_job_resource_name
    )

    # Offline user data jobs may take up to 24 hours to complete, so
    # instead of waiting for the job to complete, retrieves and displays
    # the job status once and then prints the query to use to check the job
    # again later.
    check_job_status(client, customer_id, offline_user_data_job_resource_name)


def create_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
):
    """Creates an offline user data job for uploading store sales transactions.

    Args:
        client: An initialized Google Ads API client.
        offline_user_data_job_service: The offline user data job service client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_type: Optional type of offline user data in the
            job (first party or third party).
        external_id: Optional external ID for the offline user data job.
        advertiser_upload_date_time: Optional date and time the advertiser
            uploaded data to the partner. Only required for third party uploads.
        bridge_map_version_id: Optional version of partner IDs to be used for
            uploads. Only required for third party uploads.
        partner_id: Optional ID of the third party partner. Only required for
            third party uploads.
        custom_key: A custom key str to segment store sales conversions. Only
            required after creating a custom key and custom values in the
            account.

    Returns:
        The string resource name of the created job.
    """
    # TIP: If you are migrating from the AdWords API, please note that Google
    # Ads API uses the term "fraction" instead of "rate". For example,
    # loyalty_rate in the AdWords API is called loyalty_fraction in the Google
    # Ads API.

    # Create a new offline user data job.
    offline_user_data_job = client.get_type("OfflineUserDataJob")
    offline_user_data_job.type_ = offline_user_data_job_type
    if external_id is not None:
        offline_user_data_job.external_id = external_id

    # Please refer to https://support.google.com/google-ads/answer/7506124 for
    # additional details.
    store_sales_metadata = offline_user_data_job.store_sales_metadata
    # Set the fraction of your overall sales that you (or the advertiser,
    # in the third party case) can associate with a customer (email, phone
    # number, address, etc.) in your database or loyalty program.
    # For example, set this to 0.7 if you have 100 transactions over 30
    # days, and out of those 100 transactions, you can identify 70 by an
    # email address or phone number.
    store_sales_metadata.loyalty_fraction = 0.7
    # Set the fraction of sales you're uploading out of the overall sales
    # that you (or the advertiser, in the third party case) can associate
    # with a customer. In most cases, you will set this to 1.0.
    # Continuing the example above for loyalty fraction, a value of 1.0 here
    # indicates that you are uploading all 70 of the transactions that can
    # be identified by an email address or phone number.
    store_sales_metadata.transaction_upload_fraction = 1.0

    if custom_key:
        store_sales_metadata.custom_key = custom_key

    if (
        offline_user_data_job_type
        == client.enums.OfflineUserDataJobTypeEnum.STORE_SALES_UPLOAD_THIRD_PARTY
    ):
        # Create additional metadata required for uploading third party data.
        store_sales_third_party_metadata = (
            store_sales_metadata.third_party_metadata
        )
        # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
        store_sales_third_party_metadata.advertiser_upload_date_time = (
            advertiser_upload_date_time
        )
        # Set the fraction of transactions you received from the advertiser
        # that have valid formatting and values. This captures any transactions
        # the advertiser provided to you but which you are unable to upload to
        # Google due to formatting errors or missing data.
        # In most cases, you will set this to 1.0.
        store_sales_third_party_metadata.valid_transaction_fraction = 1.0
        # Set the fraction of valid transactions (as defined above) you
        # received from the advertiser that you (the third party) have matched
        # to an external user ID on your side.
        # In most cases, you will set this to 1.0.
        store_sales_third_party_metadata.partner_match_fraction = 1.0
        # Set the fraction of transactions you (the third party) are uploading
        # out of the transactions you received from the advertiser that meet
        # both of the following criteria:
        # 1. Are valid in terms of formatting and values. See valid transaction
        # fraction above.
        # 2. You matched to an external user ID on your side. See partner match
        # fraction above.
        # In most cases, you will set this to 1.0.
        store_sales_third_party_metadata.partner_upload_fraction = 1.0
        # Set the version of partner IDs to be used for uploads.
        # Please speak with your Google representative to get the values to use
        # for the bridge map version and partner IDs.
        store_sales_third_party_metadata.bridge_map_version_id = (
            bridge_map_version_id
        )
        # Set the third party partner ID uploading the transactions.
        store_sales_third_party_metadata.partner_id = partner_id

    create_offline_user_data_job_response = (
        offline_user_data_job_service.create_offline_user_data_job(
            customer_id=customer_id, job=offline_user_data_job
        )
    )
    offline_user_data_job_resource_name = (
        create_offline_user_data_job_response.resource_name
    )
    print(
        "Created an offline user data job with resource name "
        f"'{offline_user_data_job_resource_name}'."
    )
    return offline_user_data_job_resource_name


def add_transactions_to_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_resource_name,
    conversion_action_id,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
    ad_user_data_consent,
    ad_personalization_consent,
):
    """Add operations to the job for a set of sample transactions.

    Args:
        client: An initialized Google Ads API client.
        offline_user_data_job_service: The offline user data job service client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_resource_name: The string resource name of the
            offline user data job that will receive the transactions.
        conversion_action_id: The ID of a store sales conversion action.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.
        ad_user_data_consent: The consent status for ad user data for all
            members in the job.
        ad_personalization_consent: The personalization consent status for ad
            user data for all members in the job.
    """
    # Construct some sample transactions.
    operations = build_offline_user_data_job_operations(
        client,
        customer_id,
        conversion_action_id,
        custom_value,
        item_id,
        merchant_center_account_id,
        country_code,
        language_code,
        quantity,
        ad_user_data_consent,
        ad_personalization_consent,
    )

    # Constructs a request with partial failure enabled to add the operations
    # to the offline user data job, and enable_warnings set to true to retrieve
    # warnings.
    request = client.get_type("AddOfflineUserDataJobOperationsRequest")
    request.resource_name = offline_user_data_job_resource_name
    request.enable_partial_failure = True
    request.enable_warnings = True
    request.operations = operations

    response = (
        offline_user_data_job_service.add_offline_user_data_job_operations(
            request=request,
        )
    )

    # Print the error message for any partial failure error that is returned.
    if response.partial_failure_error:
        print_google_ads_failures(response.partial_failure_error)
    else:
        print(
            f"Successfully added {len(operations)} to the offline user data "
            "job."
        )

    # Print the message for any warnings that are returned.
    if response.warning:
        print_google_ads_failures(response.warning)


def print_google_ads_failures(client, status):
    """Prints the details for partial failure errors and warnings.

    Both partial failure errors and warnings are returned as Status instances,
    which include serialized GoogleAdsFailure objects. Here we deserialize
    each GoogleAdsFailure and print the error details it includes.

    Args:
        client: An initialized Google Ads API client.
        status: a google.rpc.Status instance.
    """
    for detail in status.details:
        google_ads_failure = client.get_type("GoogleAdsFailure")
        # Retrieve the class definition of the GoogleAdsFailure instance
        # with type() in order to use the "deserialize" class method to parse
        # the detail string into a protobuf message instance.
        failure_instance = type(google_ads_failure).deserialize(detail.value)
        for error in failure_instance.errors:
            print(
                "A partial failure or warning at index "
                f"{error.location.field_path_elements[0].index} occurred.\n"
                f"Message: {error.message}\n"
                f"Code: {error.error_code}"
            )


def build_offline_user_data_job_operations(
    client,
    customer_id,
    conversion_action_id,
    custom_value,
    item_id,
    merchant_center_account_id,
    country_code,
    language_code,
    quantity,
    ad_user_data_consent,
    ad_personalization_consent,
):
    """Create offline user data job operations for sample transactions.

    Args:
        client: An initialized Google Ads API client.
        customer_id: The Google Ads customer ID.
        conversion_action_id: The ID of a store sales conversion action.
        custom_value: A custom value str to segment store sales conversions.
            Only required after creating a custom key and custom values in the
            account.
        item_id: Optional str ID of the product. Either the Merchant Center Item
            ID or the Global Trade Item Number (GTIN). Only required if
            uploading with item attributes.
        merchant_center_account_id: Optional Merchant Center Account ID. Only
            required if uploading with item attributes.
        country_code: Optional two-letter country code of the location associated
            with the feed where your items are uploaded. Only required if
            uploading with item attributes.
        language_code: Optional two-letter country code of the language
            associated with the feed where your items are uploaded. Only
            required if uploading with item attributes.
        quantity: Optional number of items sold. Only required if uploading with
            item attributes.
        ad_user_data_consent: The consent status for ad user data for all
            members in the job.
        ad_personalization_consent: The personalization consent status for ad
            user data for all members in the job.

    Returns:
        A list of OfflineUserDataJobOperations.
    """
    # Create the first transaction for upload with an email address and state.
    user_data_with_email_address_operation = client.get_type(
        "OfflineUserDataJobOperation"
    )
    user_data_with_email_address = user_data_with_email_address_operation.create
    email_identifier = client.get_type("UserIdentifier")
    # Hash normalized email addresses based on SHA-256 hashing algorithm.
    email_identifier.hashed_email = normalize_and_hash("dana@example.com")
    state_identifier = client.get_type("UserIdentifier")
    state_identifier.address_info.state = "NY"
    user_data_with_email_address.user_identifiers.extend(
        [email_identifier, state_identifier]
    )
    user_data_with_email_address.transaction_attribute.conversion_action = (
        client.get_service("ConversionActionService").conversion_action_path(
            customer_id, conversion_action_id
        )
    )
    user_data_with_email_address.transaction_attribute.currency_code = "USD"
    # Convert the transaction amount from $200 USD to micros.
    user_data_with_email_address.transaction_attribute.transaction_amount_micros = (
        200000000
    )
    # Specify the date and time of the transaction. The format is
    # "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional timezone
    # offset from UTC. If the offset is absent, the API will use the account's
    # timezone as default. Examples: "2018-03-05 09:15:00" or
    # "2018-02-01 14:34:30+03:00".
    user_data_with_email_address.transaction_attribute.transaction_date_time = (
        datetime.now() - datetime.timedelta(months=1)
    ).strftime("%Y-%m-%d %H:%M:%S")

    # Specifies whether user consent was obtained for the data you are
    # uploading. For more details, see:
    # https://www.google.com/about/company/user-consent-policy
    if ad_user_data_consent:
        user_data_with_email_address.consent.ad_user_data = (
            client.enums.ConsentStatusEnum[ad_user_data_consent]
        )
    if ad_personalization_consent:
        user_data_with_email_address.consent.ad_personalization = (
            client.enums.ConsentStatusEnum[ad_personalization_consent]
        )

    if custom_value:
        user_data_with_email_address.transaction_attribute.custom_value = (
            custom_value
        )

    # Create the second transaction for upload based on a physical address.
    user_data_with_physical_address_operation = client.get_type(
        "OfflineUserDataJobOperation"
    )
    user_data_with_physical_address = (
        user_data_with_physical_address_operation.create
    )
    address_identifier = client.get_type("UserIdentifier")
    # First and last name must be normalized and hashed.
    address_identifier.address_info.hashed_first_name = normalize_and_hash(
        "Dana"
    )
    address_identifier.address_info.hashed_last_name = normalize_and_hash(
        "Quinn"
    )
    # Country and zip codes are sent in plain text.
    address_identifier.address_info.country_code = "US"
    address_identifier.address_info.postal_code = "10011"
    user_data_with_physical_address.user_identifiers.append(address_identifier)
    user_data_with_physical_address.transaction_attribute.conversion_action = (
        client.get_service("ConversionActionService").conversion_action_path(
            customer_id, conversion_action_id
        )
    )
    user_data_with_physical_address.transaction_attribute.currency_code = "EUR"
    # Convert the transaction amount from 450 EUR to micros.
    user_data_with_physical_address.transaction_attribute.transaction_amount_micros = (
        450000000
    )
    # Specify the date and time of the transaction. This date and time
    # will be interpreted by the API using the Google Ads customer's
    # time zone. The date/time must be in the format
    # "yyyy-MM-dd hh:mm:ss".
    user_data_with_physical_address.transaction_attribute.transaction_date_time = (
        datetime.now() - datetime.timedelta(days=1)
    ).strftime(
        "%Y-%m-%d %H:%M:%S"
    )

    # Optional: If uploading data with item attributes, also assign these
    # values in the transaction attribute
    if item_id:
        item_attribute = (
            user_data_with_physical_address.transaction_attribute.item_attribute
        )
        item_attribute.item_id = item_id
        item_attribute.merchant_id = merchant_center_account_id
        item_attribute.country_code = country_code
        item_attribute.language_code = language_code
        item_attribute.quantity = quantity

    return [
        user_data_with_email_address_operation,
        user_data_with_physical_address_operation,
    ]


def normalize_and_hash(s):
    """Normalizes and hashes a string with SHA-256.

    Args:
        s: The string to perform this operation on.

    Returns:
        A normalized (lowercase, remove whitespace) and SHA-256 hashed string.
    """
    return hashlib.sha256(s.strip().lower().encode()).hexdigest()


def check_job_status(client, customer_id, offline_user_data_job_resource_name):
    """Retrieves, checks, and prints the status of the offline user data job.

    Args:
        client: An initialized Google Ads API client.
        customer_id: The Google Ads customer ID.
        offline_user_data_job_resource_name: The resource name of the job whose
            status you wish to check.
    """
    # Get the GoogleAdsService client.
    googleads_service = client.get_service("GoogleAdsService")

    # Construct a query to fetch the job status.
    query = f"""
        SELECT
          offline_user_data_job.resource_name,
          offline_user_data_job.id,
          offline_user_data_job.status,
          offline_user_data_job.type,
          offline_user_data_job.failure_reason
        FROM offline_user_data_job
        WHERE offline_user_data_job.resource_name =
          '{offline_user_data_job_resource_name}'"""

    # Issue the query and get the GoogleAdsRow containing the job.
    googleads_row = next(
        iter(googleads_service.search(customer_id=customer_id, query=query))
    )
    offline_user_data_job = googleads_row.offline_user_data_job

    offline_user_data_job_type_enum = (
        client.enums.OfflineUserDataJobTypeEnum.OfflineUserDataJobType
    )
    offline_user_data_job_status_enum = (
        client.enums.OfflineUserDataJobStatusEnum.OfflineUserDataJobStatus
    )

    job_status = offline_user_data_job.status
    print(
        f"Offline user data job ID {offline_user_data_job.id} with type "
        f"'{offline_user_data_job_type_enum.Name(offline_user_data_job.type)}' "
        f"has status {offline_user_data_job_status_enum.Name(job_status)}."
    )

    offline_user_data_job_status_enum = (
        client.enums.OfflineUserDataJobStatusEnum
    )
    if job_status == offline_user_data_job_status_enum.FAILED:
        print(f"\tFailure reason: {offline_user_data_job.failure_reason}")
    elif (
        job_status == offline_user_data_job_status_enum.PENDING
        or job_status == offline_user_data_job_status_enum.RUNNING
    ):
        print(
            "\nTo check the status of the job periodically, use the "
            f"following GAQL query with GoogleAdsService.Search:\n{query}\n"
        )
    elif job_status == offline_user_data_job_status_enum.SUCCESS:
        print("\nThe requested job has completed successfully.")
    else:
        raise ValueError("Requested job has UNKNOWN or UNSPECIFIED status.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="This example uploads offline data for store sales "
        "transactions."
    )
    # 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(
        "-a",
        "--conversion_action_id",
        type=int,
        required=True,
        help="The ID of a store sales conversion action.",
    )
    group = parser.add_mutually_exclusive_group(required=False)
    group.add_argument(
        "-k",
        "--custom_key",
        type=str,
        help="Only required after creating a custom key and custom values in "
        "the account. Custom key and values are used to segment store sales "
        "conversions. This measurement can be used to provide more advanced "
        "insights. If provided, a custom value must also be provided",
    )
    group.add_argument(
        "-v",
        "--custom_value",
        type=str,
        help="Only required after creating a custom key and custom values in "
        "the account. Custom key and values are used to segment store sales "
        "conversions. This measurement can be used to provide more advanced "
        "insights. If provided, a custom key must also be provided",
    )
    parser.add_argument(
        "-o",
        "--offline_user_data_job_type",
        type=int,
        required=False,
        default=googleads_client.enums.OfflineUserDataJobTypeEnum.STORE_SALES_UPLOAD_FIRST_PARTY,
        help="Optional type of offline user data in the job (first party or "
        "third party). If you have an official store sales partnership with "
        "Google, use STORE_SALES_UPLOAD_THIRD_PARTY. Otherwise, defaults to "
        "STORE_SALES_UPLOAD_FIRST_PARTY.",
    )
    parser.add_argument(
        "-e",
        "--external_id",
        type=int,
        required=False,
        default=None,
        help="Optional, but recommended, external ID for the offline user data "
        "job.",
    )
    parser.add_argument(
        "-d",
        "--advertiser_upload_date_time",
        type=str,
        required=False,
        default=None,
        help="Optional date and time the advertiser uploaded data to the "
        "partner. Only required for third party uploads. The format is "
        "'yyyy-mm-dd hh:mm:ss+|-hh:mm', e.g. '2021-01-01 12:32:45-08:00'.",
    )
    parser.add_argument(
        "-b",
        "--bridge_map_version_id",
        type=str,
        required=False,
        default=None,
        help="Optional version of partner IDs to be used for uploads. Only "
        "required for third party uploads.",
    )
    parser.add_argument(
        "-p",
        "--partner_id",
        type=int,
        required=False,
        default=None,
        help="Optional ID of the third party partner. Only required for third "
        "party uploads.",
    )
    parser.add_argument(
        "-i",
        "--item_id",
        type=str,
        required=False,
        default=None,
        help="Optional ID of the product. Either the Merchant Center Item ID "
        "or the Global Trade Item Number (GTIN). Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-m",
        "--merchant_center_account_id",
        type=int,
        required=False,
        default=None,
        help="Optional Merchant Center Account ID. Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-r",
        "--country_code",
        type=str,
        required=False,
        default=None,
        help="Optional two-letter country code of the location associated with "
        "the feed where your items are uploaded. Only required if uploading "
        "with item attributes.",
    )
    parser.add_argument(
        "-l",
        "--language_code",
        type=str,
        required=False,
        default=None,
        help="Optional two-letter language code of the language associated "
        "with the feed where your items are uploaded. Only required if "
        "uploading with item attributes.",
    )
    parser.add_argument(
        "-q",
        "--quantity",
        type=int,
        required=False,
        default=1,
        help="Optional number of items sold. Only required if uploading with "
        "item attributes.",
    )
    parser.add_argument(
        "-d",
        "--ad_user_data_consent",
        type=str,
        choices=[e.name for e in googleads_client.enums.ConsentStatusEnum],
        help=(
            "The data consent status for ad user data for all members in "
            "the job."
        ),
    )
    parser.add_argument(
        "-p",
        "--ad_personalization_consent",
        type=str,
        choices=[e.name for e in googleads_client.enums.ConsentStatusEnum],
        help=(
            "The personalization consent status for ad user data for all "
            "members in the job."
        ),
    )
    args = parser.parse_args()

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

    # Additional check to make sure that custom_key and custom_value are either
    # not provided or both provided together.
    required_together = ("custom_key", "custom_value")
    required_custom_vals = [
        getattr(args, field, None) for field in required_together
    ]
    if any(required_custom_vals) and not all(required_custom_vals):
        parser.error(
            "--custom_key (-k) and --custom_value (-v) must be passed "
            "in together"
        )

    try:
        main(
            googleads_client,
            args.customer_id,
            args.conversion_action_id,
            args.offline_user_data_job_type,
            args.external_id,
            args.advertiser_upload_date_time,
            args.bridge_map_version_id,
            args.partner_id,
            args.custom_key,
            args.custom_value,
            args.item_id,
            args.merchant_center_account_id,
            args.country_code,
            args.language_code,
            args.quantity,
            args.ad_user_data_consent,
            args.ad_personalization_consent,
        )
    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"\tError 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 2020 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.
#
# Uploads offline data for store sales transactions.
#
# This feature is only available to allowlisted accounts. See
# https://support.google.com/google-ads/answer/7620302 for more details.

require 'date'
require 'digest'
require 'google/ads/google_ads'
require 'optparse'

def upload_store_sales_transactions(
  customer_id,
  offline_user_data_job_type,
  conversion_action_id,
  external_id,
  advertiser_upload_date_time,
  bridge_map_version_id,
  partner_id,
  custom_key,
  custom_value,
  item_id,
  merchant_center_account_id,
  region_code,
  language_code,
  quantity,
  ad_user_data_consent,
  ad_personalization_consent)
  # GoogleAdsClient will read a config file from
  # ENV['HOME']/google_ads_config.rb when called without parameters
  client = Google::Ads::GoogleAds::GoogleAdsClient.new
  offline_user_data_job_service = client.service.offline_user_data_job

  # Creates an offline user data job for uploading transactions.
  offline_user_data_job_resource_name = create_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_type,
    external_id,
    advertiser_upload_date_time,
    bridge_map_version_id,
    partner_id,
    custom_key,
  )

  # Add transactions to the job
  add_transactions_to_offline_user_data_job(
    client,
    offline_user_data_job_service,
    customer_id,
    offline_user_data_job_resource_name,
    conversion_action_id,
    custom_value,
    ad_user_data_consent,
    ad_personalization_consent
  )

  # Issues an asynchronous request to run the offline user data job.
  offline_user_data_job_service.run_offline_user_data_job(
    resource_name: offline_user_data_job_resource_name,
  )

  puts "Sent request to asynchronously run offline user data job: " \
    "#{offline_user_data_job_resource_name}"

  # Offline user data jobs may take up to 24 hours to complete, so instead of
  # waiting for the job to complete, retrieves and displays the job status once
  # and then prints the query to use to check the job again later.
  check_job_status(client, customer_id, offline_user_data_job_resource_name)
end

# Creates an offline user data job for uploading store sales transactions.
def create_offline_user_data_job(
  client,
  offline_user_data_job_service,
  customer_id,
  offline_user_data_job_type,
  external_id,
  advertiser_upload_date_time,
  bridge_map_version_id,
  partner_id,
  custom_key)
  # TIP: If you are migrating from the AdWords API, please note tha Google Ads
  # API uses the term "fraction" instead of "rate". For example, loyalty_rate
  # in the AdWords API is called loyalty_fraction in the Google Ads API.
  # Please refer to https://support.google.com/google-ads/answer/7506124 for
  # additional details.
  store_sales_metadata = client.resource.store_sales_metadata do |s|
    # Sets the fraction of your overall sales that you (or the advertiser, in
    # the third party case) can associate with a customer (email, phone number,
    # address, etc.) in your database or loyalty program.
    # For example, set this to 0.7 if you have 100 transactions over 30 days,
    # and out of those 100 transactions, you can identify 70 by an email address
    # or phone number.
    s.loyalty_fraction = 0.7
    # Sets the fraction of sales you're uploading out of the overall sales that
    # you (or the advertiser, in the third party case) can associate with a
    # customer. In most cases, you will set this to 1.0.
    # Continuing the example above for loyalty fraction, a value of 1.0 here
    # indicates that you are uploading all 70 of the transactions that can be
    # identified by an email address or phone number.
    s.transaction_upload_fraction = 1.0
    s.custom_key = custom_key unless custom_key.nil?
  end

  # Creates additional metadata required for uploading third party data.
  if offline_user_data_job_type == :STORE_SALES_UPLOAD_THIRD_PARTY
    store_sales_metadata.third_party_metadata =
      client.resource.store_sales_third_party_metadata do |t|
      # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
      t.advertiser_upload_date_time = advertiser_upload_date_time
      # Sets the fraction of transactions you received from the advertiser
      # that have valid formatting and values. This captures any transactions
      # the advertiser provided to you but which you are unable to upload to
      # Google due to formatting errors or missing data.
      # In most cases, you will set this to 1.0.
      t.valid_transaction_fraction = 1.0
      # Sets the fraction of valid transactions (as defined above) you received
      # from the advertiser that you (the third party) have matched to an
      # external user ID on your side.
      # In most cases, you will set this to 1.0.
      t.partner_match_fraction = 1.0
      # Sets the fraction of transactions you (the third party) are uploading
      # out of the transactions you received from the advertiser that meet both
      # of the following criteria:
      # 1. Are valid in terms of formatting and values. See valid transaction
      # fraction above.
      # 2. You matched to an external user ID on your side. See partner match
      # fraction above.
      # In most cases, you will set this to 1.0.
      t.partner_upload_fraction = 1.0
      # Please speak with your Google representative to get the values to use
      # for the bridge map version and partner IDs.
      # Sets the version of partner IDs to be used for uploads.
      t.bridge_map_version_id = bridge_map_version_id
      # Sets the third party partner ID uploading the transactions.
      t.partner_id = partner_id.to_i
    end
  end

  # Creates a new offline user data job.
  offline_user_data_job = client.resource.offline_user_data_job do |job|
    job.type = offline_user_data_job_type
    job.store_sales_metadata = store_sales_metadata
  end

  unless external_id.nil?
    offline_user_data_job.external_id = external_id.to_i
  end

  # Issues a request to create the offline user data job.
  response = offline_user_data_job_service.create_offline_user_data_job(
    customer_id: customer_id,
    job: offline_user_data_job,
  )

  offline_user_data_job_resource_name = response.resource_name
  puts "Created an offline user data job with resource name: " \
    "#{offline_user_data_job_resource_name}."

  offline_user_data_job_resource_name
end

# Adds operations to the job for a set of sample transactions.
def add_transactions_to_offline_user_data_job(
  client,
  offline_user_data_job_service,
  customer_id,
  offline_user_data_job_resource_name,
  conversion_action_id,
  custom_value,
  ad_user_data_consent,
  ad_personalization_consent)
  # Constructs the operation for each transaction.
  user_data_job_operations = build_offline_user_data_job_operations(
    client, customer_id, conversion_action_id, custom_value, ad_user_data_consent,
    ad_personalization_consent)

  # Issues a request to add the operations to the offline user data job.
  response = offline_user_data_job_service.add_offline_user_data_job_operations(
    resource_name: offline_user_data_job_resource_name,
    operations: user_data_job_operations,
    enable_partial_failure: true,
    enable_warnings: true,
  )

  # Prints errors if any partial failure error is returned.
  if response.partial_failure_error
    failures = client.decode_partial_failure_error(response.partial_failure_error)
    failures.each do |failure|
      failure.errors.each do |error|
        human_readable_error_path = error
          .location
          .field_path_elements
          .map { |location_info|
            if location_info.index
              "#{location_info.field_name}[#{location_info.index}]"
            else
              "#{location_info.field_name}"
            end
          }.join(" > ")

        errmsg =  "error occured while adding operations " \
          "#{human_readable_error_path}" \
          " with value: #{error.trigger&.string_value}" \
          " because #{error.message.downcase}"
        puts errmsg
      end
    end
  end

  if response.warning
    # Convert to a GoogleAdsFailure.
    warnings = client.decode_warning(response.warning)
    puts "Encountered #{warnings.errors.size} warning(s)."
  end

  puts "Successfully added #{user_data_job_operations.size} operations to " \
    "the offline user data job."
end

# Creates a list of offline user data job operations for sample transactions.
def build_offline_user_data_job_operations(
  client,
  customer_id,
  conversion_action_id,
  custom_value,
  ad_user_data_consent,
  ad_personalization_consent)
  operations = []

  # Creates the first transaction for upload based on an email address
  # and state.
  operations << client.operation.create_resource.offline_user_data_job do |op|
    op.user_identifiers << client.resource.user_identifier do |id|
      # Email addresses must be normalized and hashed.
      id.hashed_email = normalize_and_hash("dana@example.com")
    end
    op.user_identifiers << client.resource.user_identifier do |id|
      id.address_info = client.resource.offline_user_address_info do |info|
        info.state = "NY"
      end
    end
    op.transaction_attribute = client.resource.transaction_attribute do |t|
      t.conversion_action = client.path.conversion_action(
        customer_id, conversion_action_id)
      t.currency_code = "USD"
      # Converts the transaction amount from $200 USD to micros.
      t.transaction_amount_micros = 200_000_000
      # Specifies the date and time of the transaction. The format is
      # "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional timezone
      # offset from UTC. If the offset is absent, the API will use the
      # account's timezone as default. Examples: "2018-03-05 09:15:00" or
      # "2018-02-01 14:34:30+03:00".
      t.transaction_date_time = "2020-05-01 23:52:12"
      t.custom_value = custom_value unless custom_value.nil?
    end
    if !ad_user_data_consent.nil? || !ad_personalization_consent.nil?
      op.consent = client.resource.consent do |c|
        # Specifies whether user consent was obtained for the data you are
        # uploading. For more details, see:
        # https://www.google.com/about/company/user-consent-policy
        unless ad_user_data_consent.nil?
          c.ad_user_data = ad_user_data_consent
        end
        unless ad_personalization_consent.nil?
          c.ad_personalization = ad_personalization_consent
        end
      end
    end
  end

  # Creates the second transaction for upload based on a physical address.
  operations << client.operation.create_resource.offline_user_data_job do |op|
    op.user_identifiers << client.resource.user_identifier do |id|
      id.address_info = client.resource.offline_user_address_info do |info|
        # First and last name must be normalized and hashed.
        info.hashed_first_name = normalize_and_hash("Dana")
        info.hashed_last_name = normalize_and_hash("Quinn")
        # Country code and zip code are sent in plain text.
        info.country_code = "US"
        info.postal_code = "10011"
      end
    end
    op.transaction_attribute = client.resource.transaction_attribute do |t|
      t.conversion_action = client.path.conversion_action(
        customer_id, conversion_action_id)
      t.currency_code = "EUR"
      # Converts the transaction amount from 450 EUR to micros.
      t.transaction_amount_micros = 450_000_000
      # Specifies the date and time of the transaction. This date and time will
      # be interpreted by the API using the Google Ads customer's time zone.
      # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
      t.transaction_date_time = "2020-05-14 19:07:02"
      t.custom_value = custom_value unless custom_value.nil?
      if item_id
        t.item_attribute = client.resource.item_attribute do |item|
          item.item_id = item_id
          item.merchant_id = merchant_center_account_id.to_i
          item.region_code = region_code
          item.language_code = language_code
          item.quantity = quantity.to_i
        end
      end
    end
  end

  # Returns the operations containing the two transactions.
  operations
end

# Returns the result of normalizing and then hashing the string.
# Private customer data must be hashed during upload, as described at
# https://support.google.com/google-ads/answer/7506124.
def normalize_and_hash(str)
  Digest::SHA256.hexdigest(str.strip.downcase)
end

# Retrieves, checks, and prints the status of the offline user data job.
def check_job_status(
  client,
  customer_id,
  offline_user_data_job_resource_name)
  # Creates a query that retrieves the offline user data.
  query = <<~QUERY
    SELECT offline_user_data_job.resource_name,
      offline_user_data_job.id,
      offline_user_data_job.status,
      offline_user_data_job.type,
      offline_user_data_job.failure_reason
    FROM offline_user_data_job
    WHERE offline_user_data_job.resource_name = "#{offline_user_data_job_resource_name}"
  QUERY

  puts query

  # Issues a search stream request.
  responses = client.service.google_ads.search_stream(
    customer_id: customer_id,
    query: query,
  )

  # Prints out some information about the offline user data.
  offline_user_data_job = responses.first.results.first.offline_user_data_job
  puts "Offline user data job ID #{offline_user_data_job.id} " \
    "with type #{offline_user_data_job.type} " \
    "has status: #{offline_user_data_job.status}"

  if offline_user_data_job.status == :FAILED
    puts "  Failure reason: #{offline_user_data_job.failure_reason}"
  elsif offline_user_data_job.status == :PENDING \
      || offline_user_data_job.status == :RUNNING
    puts "To check the status of the job periodically, use the following GAQL " \
      "query with google_ads.search:"
    puts query
  end
end

if __FILE__ == $0
  options = {}
  # The following parameter(s) should be provided to run the example. You can
  # either specify these by changing the INSERT_XXX_ID_HERE values below, or on
  # the command line.
  #
  # Parameters passed on the command line will override any parameters set in
  # code.
  #
  # Running the example with -h will print the command line usage.
  options[:customer_id] = 'INSERT_CUSTOMER_ID_HERE'
  options[:conversion_action_id] = 'INSERT_CONVERSION_ACTION_ID_HERE'
  options[:offline_user_data_job_type] = "STORE_SALES_UPLOAD_FIRST_PARTY"

  OptionParser.new do |opts|
    opts.banner = sprintf('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('-c', '--conversion-action-id CONVERSION-ACTION-ID', String,
      'The ID of a store sales conversion action') do |v|
      options[:conversion_action_id] = v
    end

    opts.on('-T', '--offline-user-data-job-type OFFLINE-USER-DATA-JOB-TYPE', String,
      '(Optional) The type of user data in the job (first or third party). ' \
      'If you have an official store sales partnership with Google, ' \
      'use STORE_SALES_UPLOAD_THIRD_PARTY. Otherwise, use ' \
      'STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.') do |v|
      options[:offline_user_data_job_type] = v
    end

    opts.on('-E', '--external-id EXTERNAL-ID', String,
      '(Optional, but recommended) external ID to identify the offline ' \
      'user data job') do |v|
      options[:external_id] = v
    end

    opts.on('-U', '--advertiser-upload-date-time ADVERTISER-UPLOAD-DATE-TIME', String,
      '(Only required if uploading third party data) Specify the date and time ' \
      'the advertiser uploaded data to the partner. ' \
      'The format is "yyyy-mm-dd hh:mm:ss"') do |v|
      options[:advertiser_upload_date_time] = v
    end

    opts.on('-B', '--bridge-map-version-id BRIDGE-MAP-VERSION-ID', String,
      '(Only required if uploading third party data) ' \
      'The version of partner IDs to be used for uploads.') do |v|
      options[:bridge_map_version_id] = v
    end

    opts.on('-P', '--partner-id PARTNER-ID', String,
      '(Only required if uploading third party data) ' \
      'The ID of the third party partner. ') do |v|
      options[:partner_id] = v
    end

    opts.on('-k' '--custom-key CUSTOM-KEY', String,
      'Only required after creating a custom key and custom values in ' \
      'the account. Custom key and values are used to segment store sales ' \
      'conversions. This measurement can be used to provide more advanced ' \
      'insights. If provided, a custom value must also be provided') do |v|
      options[:custom_key] = v
    end

    opts.on('-v' '--custom-value CUSTOM-VALUE', String,
      'Only required after creating a custom key and custom values in ' \
      'the account. Custom key and values are used to segment store sales ' \
      'conversions. This measurement can be used to provide more advanced ' \
      'insights. If provided, a custom key must also be provided') do |v|
      options[:custom_value] = v
    end

    opts.on('-i', '--item-id ITEM-ID', String,
      'Optional: Specify a unique identifier of a product, either the ' \
      'Merchant Center Item ID or Global Trade Item Number (GTIN). ' \
      'Only required if uploading with item attributes.') do |v|
      options[:item_id] = v
    end

    opts.on('-m', '--merchant-center-account-id MERCHANT-CENTER-ACCOUNT-ID', String,
      'Optional: Specify a Merchant Center Account ID. Only required if ' \
      'uploading with item attributes.') do |v|
      options[:merchant_center_account_id] = v
    end

    opts.on('-r', '--region-code REGION-CODE', String,
      'Optional: Specify a two-letter region code of the location associated ' \
      'with the feed where your items are uploaded. Only required if ' \
      'uploading with item attributes.') do |v|
      options[:region_code] = v
    end

    opts.on('-L', '--language-code LANGUAGE-CODE', String,
      'Optional: Specify a two-letter language code of the language ' \
      'associated with the feed where your items are uploaded. Only required ' \
      'if uploading with item attributes.') do |v|
      options[:language_code] = v
    end

    opts.on('-q', '--quantity QUANTITY', String,
      'Optional: Specify a number of items sold. Only required if uploading ' \
      'with item attributes.') do |v|
      options[:quantity] = v
    end

    opts.on('-d', '--ad-user-data-consent [AD-USER-DATA_CONSENT]', String,
        'The personalization consent status for ad user data for all members in the job.' \
        'e.g. UNKNOWN, GRANTED, DENIED') do |v|
      options[:ad_user_data_consent] = v
    end

    opts.on('-p', '--ad-personalization-consent [AD-PERSONALIZATION-CONSENT]', String,
        'The personalization consent status for ad user data for all members in the job.' \
        'e.g. UNKNOWN, GRANTED, DENIED') do |v|
      options[:ad_personalization_consent] = v
    end

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

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

  begin
    upload_store_sales_transactions(
      options.fetch(:customer_id).tr("-", ""),
      options[:offline_user_data_job_type].to_sym,
      options.fetch(:conversion_action_id),
      options[:external_id],
      options[:advertiser_upload_date_time],
      options[:bridge_map_version_id],
      options[:partner_id],
      options[:custom_key],
      options[:custom_value],
      options[:item_id],
      options[:merchant_center_account_id],
      options[:region_code],
      options[:language_code],
      options[:quantity],
      options[:ad_user_data_consent],
      options[:ad_personalization_consent],
    )
  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
  end
end

      

Perl

#!/usr/bin/perl -w
#
# Copyright 2020, 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 uploads offline data for store sales transactions.
#
# This feature is only available to allowlisted accounts.
# See https://support.google.com/google-ads/answer/7620302 for more details.

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::V17::Resources::OfflineUserDataJob;
use Google::Ads::GoogleAds::V17::Common::Consent;
use Google::Ads::GoogleAds::V17::Common::ItemAttribute;
use Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo;
use Google::Ads::GoogleAds::V17::Common::StoreSalesMetadata;
use Google::Ads::GoogleAds::V17::Common::StoreSalesThirdPartyMetadata;
use Google::Ads::GoogleAds::V17::Common::TransactionAttribute;
use Google::Ads::GoogleAds::V17::Common::UserData;
use Google::Ads::GoogleAds::V17::Common::UserIdentifier;
use Google::Ads::GoogleAds::V17::Enums::OfflineUserDataJobTypeEnum
  qw(STORE_SALES_UPLOAD_FIRST_PARTY STORE_SALES_UPLOAD_THIRD_PARTY);
use
  Google::Ads::GoogleAds::V17::Services::OfflineUserDataJobService::OfflineUserDataJobOperation;
use Google::Ads::GoogleAds::V17::Utils::ResourceNames;

use Getopt::Long qw(:config auto_help);
use Pod::Usage;
use Cwd         qw(abs_path);
use Digest::SHA qw(sha256_hex);

use constant POLL_FREQUENCY_SECONDS => 1;
use constant POLL_TIMEOUT_SECONDS   => 60;
# If uploading data with custom key and values, specify the value.
use constant CUSTOM_VALUE => "INSERT_CUSTOM_VALUE_HERE";

# The following parameter(s) should be provided to run the example. You can
# either specify these by changing the INSERT_XXX_ID_HERE values below, or on
# the command line.
#
# Parameters passed on the command line will override any parameters set in
# code.
#
# Running the example with -h will print the command line usage.
my $customer_id          = "INSERT_CUSTOMER_ID_HERE";
my $conversion_action_id = "INSERT_CONVERSION_ACTION_ID_HERE";

# Optional: Specify the type of user data in the job (first or third party).
# If you have an official store sales partnership with Google, use
# STORE_SALES_UPLOAD_THIRD_PARTY.
# Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY or omit this parameter.
my $offline_user_data_job_type = STORE_SALES_UPLOAD_FIRST_PARTY;
# Optional: Specify an external ID below to identify the offline user data job.
# If none is specified, this example will create an external ID.
my $external_id = undef;
# Optional: Specify the custom key if uploading data with custom key and values.
my $custom_key = undef;
# Optional: Specify an advertiser upload date time for third party data.
my $advertiser_upload_date_time = undef;
# Optional: Specify a bridge map version ID for third party data.
my $bridge_map_version_id = undef;
# Optional: Specify a partner ID for third party data.
my $partner_id = undef;
# Optional: Specify a unique identifier of a product, either the Merchant Center
# Item ID or Global Trade Item Number (GTIN). Only required if uploading with
# item attributes.
my $item_id = undef;
# Optional: Specify a Merchant Center Account ID. Only required if uploading
# with item attributes.
my $merchant_center_account_id = undef;
# Optional: Specify a two-letter country code of the location associated with the
# feed where your items are uploaded. Only required if uploading with item
# attributes.
my $country_code = undef;
# Optional: Specify a two-letter language code of the language associated with
# the feed where your items are uploaded. Only required if uploading with item
# attributes.
my $language_code = undef;
# Optional: Specify a number of items sold. Only required if uploading with item
# attributes.
my $quantity = 1;
# Optional: Specify the ad personalization consent status.
my $ad_personalization_consent = undef;
# Optional: Specify the ad user data consent status.
my $ad_user_data_consent = undef;

sub upload_store_sales_transactions {
  my (
    $api_client,                  $customer_id,
    $offline_user_data_job_type,  $conversion_action_id,
    $external_id,                 $custom_key,
    $advertiser_upload_date_time, $bridge_map_version_id,
    $partner_id,                  $item_id,
    $merchant_center_account_id,  $country_code,
    $language_code,               $quantity,
    $$ad_personalization_consent, $ad_user_data_consent
  ) = @_;

  my $offline_user_data_job_service = $api_client->OfflineUserDataJobService();

  # Create an offline user data job for uploading transactions.
  my $offline_user_data_job_resource_name = create_offline_user_data_job(
    $offline_user_data_job_service, $customer_id,
    $offline_user_data_job_type,    $external_id,
    $custom_key,                    $advertiser_upload_date_time,
    $bridge_map_version_id,         $partner_id
  );

  # Add transactions to the job.
  add_transactions_to_offline_user_data_job(
    $offline_user_data_job_service,       $customer_id,
    $offline_user_data_job_resource_name, $conversion_action_id,
    $custom_key,                          $item_id,
    $merchant_center_account_id,          $country_code,
    $language_code,                       $quantity,
  );

  # Issue an asynchronous request to run the offline user data job.
  my $operation_response = $offline_user_data_job_service->run({
    resourceName => $offline_user_data_job_resource_name
  });
  print "Asynchronous request to execute the added operations started.\n";
  print "Waiting until operation completes.\n";

  # poll_until_done() implements a default back-off policy for retrying. You can
  # tweak the parameters like the poll timeout seconds by passing them to the
  # poll_until_done() method. Visit the OperationService.pm file for more details.
  my $lro = $api_client->OperationService()->poll_until_done({
    name                 => $operation_response->{name},
    pollFrequencySeconds => POLL_FREQUENCY_SECONDS,
    pollTimeoutSeconds   => POLL_TIMEOUT_SECONDS
  });
  if ($lro->{done}) {
    printf "Offline user data job with resource name '%s' has finished.\n",
      $offline_user_data_job_resource_name;
  } else {
    printf
      "Offline user data job with resource name '%s' still pending after %d " .
      "seconds, continuing the execution of the code example anyway.\n",
      $offline_user_data_job_resource_name,
      POLL_TIMEOUT_SECONDS;
  }

  return 1;
}

# Creates an offline user data job for uploading store sales transactions.
# Returns the resource name of the created job.
sub create_offline_user_data_job {
  my (
    $offline_user_data_job_service, $customer_id,
    $offline_user_data_job_type,    $external_id,
    $custom_key,                    $advertiser_upload_date_time,
    $bridge_map_version_id,         $partner_id
  ) = @_;

  # TIP: If you are migrating from the AdWords API, please note that Google Ads
  # API uses the term "fraction" instead of "rate". For example, loyaltyRate in
  # the AdWords API is called loyaltyFraction in the Google Ads API.
  my $store_sales_metadata =
    # Please refer to https://support.google.com/google-ads/answer/7506124 for
    # additional details.
    Google::Ads::GoogleAds::V17::Common::StoreSalesMetadata->new({
      # Set the fraction of your overall sales that you (or the advertiser,
      # in the third party case) can associate with a customer (email, phone
      # number, address, etc.) in your database or loyalty program.
      # For example, set this to 0.7 if you have 100 transactions over 30
      # days, and out of those 100 transactions, you can identify 70 by an
      # email address or phone number.
      loyaltyFraction => 0.7,
      # Set the fraction of sales you're uploading out of the overall sales
      # that you (or the advertiser, in the third party case) can associate
      # with a customer. In most cases, you will set this to 1.0.
      # Continuing the example above for loyalty fraction, a value of 1.0 here
      # indicates that you are uploading all 70 of the transactions that can
      # be identified by an email address or phone number.
      transactionUploadFraction => 1.0
    });

  # Apply the custom key if provided.
  $store_sales_metadata->{customKey} = $custom_key if defined $custom_key;

  if ($offline_user_data_job_type eq STORE_SALES_UPLOAD_THIRD_PARTY) {
    # Create additional metadata required for uploading third party data.
    my $store_sales_third_party_metadata =
      Google::Ads::GoogleAds::V17::Common::StoreSalesThirdPartyMetadata->new({
        # The date/time must be in the format "yyyy-MM-dd hh:mm:ss".
        advertiserUploadDateTime => $advertiser_upload_date_time,

        # Set the fraction of transactions you received from the advertiser
        # that have valid formatting and values. This captures any transactions
        # the advertiser provided to you but which you are unable to upload to
        # Google due to formatting errors or missing data.
        # In most cases, you will set this to 1.0.
        validTransactionFraction => 1.0,
        # Set the fraction of valid transactions (as defined above) you received
        # from the advertiser that you (the third party) have matched to an
        # external user ID on your side.
        # In most cases, you will set this to 1.0.
        partnerMatchFraction => 1.0,

        # Set the fraction of transactions you (the third party) are uploading
        # out of the transactions you received from the advertiser that meet
        # both of the following criteria:
        # 1. Are valid in terms of formatting and values. See valid transaction
        # fraction above.
        # 2. You matched to an external user ID on your side. See partner match
        # fraction above.
        # In most cases, you will set this to 1.0.
        partnerUploadFraction => 1.0,

        # Please speak with your Google representative to get the values to use
        # for the bridge map version and partner IDs.

        # Set the version of partner IDs to be used for uploads.
        bridgeMapVersionId => $bridge_map_version_id,
        # Set the third party partner ID uploading the transactions.
        partnerId => $partner_id,
      });
    $store_sales_metadata->{thirdPartyMetadata} =
      $store_sales_third_party_metadata;
  }

  # Create a new offline user data job.
  my $offline_user_data_job =
    Google::Ads::GoogleAds::V17::Resources::OfflineUserDataJob->new({
      type               => $offline_user_data_job_type,
      storeSalesMetadata => $store_sales_metadata,
      external_id        => $external_id,
    });

  # Issue a request to create the offline user data job.
  my $create_offline_user_data_job_response =
    $offline_user_data_job_service->create({
      customerId => $customer_id,
      job        => $offline_user_data_job
    });
  my $offline_user_data_job_resource_name =
    $create_offline_user_data_job_response->{resourceName};
  printf
    "Created an offline user data job with resource name: '%s'.\n",
    $offline_user_data_job_resource_name;
  return $offline_user_data_job_resource_name;
}

# Adds operations to the job for a set of sample transactions.
sub add_transactions_to_offline_user_data_job {
  my (
    $offline_user_data_job_service,       $customer_id,
    $offline_user_data_job_resource_name, $conversion_action_id,
    $custom_key,                          $item_id,
    $merchant_center_account_id,          $country_code,
    $language_code,                       $quantity
  ) = @_;

  # Construct the operation for each transaction.
  my $user_data_job_operations = build_offline_user_data_job_operations(
    $customer_id,                $conversion_action_id,
    $custom_key,                 $item_id,
    $merchant_center_account_id, $country_code,
    $language_code,              $quantity,
    $ad_personalization_consent, $ad_user_data_consent
  );

  # Issue a request to add the operations to the offline user data job.
  my $response = $offline_user_data_job_service->add_operations({
    resourceName         => $offline_user_data_job_resource_name,
    enablePartialFailure => "true",
    # Enable warnings (optional).
    enableWarnings => "true",
    operations     => $user_data_job_operations
  });

  # Print the status message if any partial failure error is returned.
  # Note: The details of each partial failure error are not printed here, you
  # can refer to the example handle_partial_failure.pl to learn more.
  if ($response->{partialFailureError}) {
    # Extract the partial failure from the response status.
    my $partial_failure = $response->{partialFailureError}{details}[0];
    foreach my $error (@{$partial_failure->{errors}}) {
      printf "Partial failure occurred: '%s'\n", $error->{message};
    }
    printf "Encountered %d partial failure errors while adding %d operations " .
      "to the offline user data job: '%s'. Only the successfully added " .
      "operations will be executed when the job runs.\n",
      scalar @{$partial_failure->{errors}}, scalar @$user_data_job_operations,
      $response->{partialFailureError}{message};
  } else {
    printf "Successfully added %d operations to the offline user data job.\n",
      scalar @$user_data_job_operations;
  }

  # Print the number of warnings if any warnings are returned. You can access
  # details of each warning using the same approach you'd use for partial failure
  # errors.
  if ($response->{warning}) {
    # Extract the warnings from the response status.
    my $warnings_failure = $response->{warning}{details}[0];
    printf "Encountered %d warning(s).\n",
      scalar @{$warnings_failure->{errors}};
  }
}

# Creates a list of offline user data job operations for sample transactions.
# Returns a list of operations.
sub build_offline_user_data_job_operations {
  my (
    $customer_id,                $conversion_action_id,
    $custom_key,                 $item_id,
    $merchant_center_account_id, $country_code,
    $language_code,              $quantity,
    $ad_personalization_consent, $ad_user_data_consent
  ) = @_;

  # Create the first transaction for upload based on an email address and state.
  my $user_data_with_email_address =
    Google::Ads::GoogleAds::V17::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({
            # Hash normalized email addresses based on SHA-256 hashing algorithm.
            hashedEmail => normalize_and_hash('dana@example.com')}
        ),
        Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo->new({
                state => "NY"
              })})
      ],
      transactionAttribute =>
        Google::Ads::GoogleAds::V17::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V17::Utils::ResourceNames::conversion_action(
            $customer_id, $conversion_action_id
            ),
          currencyCode => "USD",
          # Convert the transaction amount from $200 USD to micros.
          transactionAmountMicros => 200000000,
          # Specify the date and time of the transaction. The format is
          # "YYYY-MM-DD HH:MM:SS[+HH:MM]", where [+HH:MM] is an optional timezone
          # offset from UTC. If the offset is absent, the API will use the
          # account's timezone as default. Examples: "2018-03-05 09:15:00"
          # or "2018-02-01 14:34:30+03:00".
          transactionDateTime => "2020-05-01 23:52:12",
        })});

  # Add consent information if specified.
  if ($ad_personalization_consent or $ad_user_data_consent) {
    # Specify whether user consent was obtained for the data you are uploading.
    # See https://www.google.com/about/company/user-consent-policy for details.
    $user_data_with_email_address->{consent} =
      Google::Ads::GoogleAds::V17::Common::Consent({
        adPersonalization => $ad_personalization_consent,
        adUserData        => $ad_user_data_consent
      });
  }

  # Optional: If uploading data with custom key and values, also assign the
  # custom value.
  if (defined($custom_key)) {
    $user_data_with_email_address->{transactionAttribute}{customValue} =
      CUSTOM_VALUE;
  }

  # Create the second transaction for upload based on a physical address.
  my $user_data_with_physical_address =
    Google::Ads::GoogleAds::V17::Common::UserData->new({
      userIdentifiers => [
        Google::Ads::GoogleAds::V17::Common::UserIdentifier->new({
            addressInfo =>
              Google::Ads::GoogleAds::V17::Common::OfflineUserAddressInfo->new({
                # First and last name must be normalized and hashed.
                hashedFirstName => normalize_and_hash("Dana"),
                hashedLastName  => normalize_and_hash("Quinn"),
                # Country code and zip code are sent in plain text.
                countryCode => "US",
                postalCode  => "10011"
              })})
      ],
      transactionAttribute =>
        Google::Ads::GoogleAds::V17::Common::TransactionAttribute->new({
          conversionAction =>
            Google::Ads::GoogleAds::V17::Utils::ResourceNames::conversion_action(
            $customer_id,
            $conversion_action_id
            ),
          currencyCode => "EUR",
          # Convert the transaction amount from 450 EUR to micros.
          transactionAmountMicros => 450000000,
          # Specify the date and time of the transaction. This date and time
          # will be interpreted by the API using the Google Ads customer's
          # time zone. The date/time must be in the format
          # "yyyy-MM-dd hh:mm:ss".
          transactionDateTime => "2020-05-14 19:07:02",
        })});

  # Optional: If uploading data with item attributes, also assign these values
  # in the transaction attribute.
  if (defined($item_id)) {
    $user_data_with_physical_address->{transactionAttribute}{itemAttribute} =
      Google::Ads::GoogleAds::V17::Common::ItemAttribute->new({
        itemId       => $item_id,
        merchantId   => $merchant_center_account_id,
        countryCode  => $country_code,
        languageCode => $language_code,
        # Quantity field should only be set when at least one of the other item
        # attributes is present.
        quantity => $quantity
      });

  }

  # Create the operations to add the two transactions.
  my $operations = [
    Google::Ads::GoogleAds::V17::Services::OfflineUserDataJobService::OfflineUserDataJobOperation
      ->new({
        create => $user_data_with_email_address
      }
      ),
    Google::Ads::GoogleAds::V17::Services::OfflineUserDataJobService::OfflineUserDataJobOperation
      ->new({
        create => $user_data_with_physical_address
      })];

  return $operations;
}

# Returns the result of normalizing and then hashing the string using the
# provided digest. Private customer data must be hashed during upload, as
# described at https://support.google.com/google-ads/answer/7506124
sub normalize_and_hash {
  my $value = shift;

  $value =~ s/^\s+|\s+$//g;
  return sha256_hex(lc $value);
}

# 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);

# Parameters passed on the command line will override any parameters set in code.
GetOptions(
  "customer_id=s"                 => \$customer_id,
  "offline_user_data_job_type=s"  => \$offline_user_data_job_type,
  "conversion_action_id=i"        => \$conversion_action_id,
  "external_id=i"                 => \$external_id,
  "custom_key=s"                  => \$custom_key,
  "advertiser_upload_date_time=s" => \$advertiser_upload_date_time,
  "bridge_map_version_id=i"       => \$bridge_map_version_id,
  "partner_id=i"                  => \$partner_id,
  "item_id=s"                     => \$item_id,
  "merchant_center_account_id=i"  => \$merchant_center_account_id,
  "country_code=s"                => \$country_code,
  "language_code=s"               => \$language_code,
  "quantity=i"                    => \$quantity,
  "ad_personalization_consent=s"  => \$ad_personalization_consent,
  "ad_user_data_consent=s"        => \$ad_user_data_consent
);

# 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, $conversion_action_id);

# Call the example.
upload_store_sales_transactions(
  $api_client,                  $customer_id =~ s/-//gr,
  $offline_user_data_job_type,  $conversion_action_id,
  $external_id,                 $custom_key,
  $advertiser_upload_date_time, $bridge_map_version_id,
  $partner_id,                  $item_id,
  $merchant_center_account_id,  $country_code,
  $language_code,               $quantity,
  $ad_personalization_consent,  $ad_user_data_consent
);

=pod

=head1 NAME

upload_store_sales_transactions

=head1 DESCRIPTION

This example uploads offline data for store sales transactions.

This feature is only available to allowlisted accounts.
See https://support.google.com/google-ads/answer/7620302 for more details.

=head1 SYNOPSIS

upload_store_sales_transactions.pl [options]

    -help                           Show the help message.
    -customer_id                    The Google Ads customer ID.
    -conversion_action_id           The ID of a store sales conversion action.
    -offline_user_data_job_type     [optional] The type of offline user data in the job (first party or third party).
                                    If you have an official store sales partnership with Google, use STORE_SALES_UPLOAD_THIRD_PARTY.
                                    Otherwise, use STORE_SALES_UPLOAD_FIRST_PARTY.
    -external_id                    [optional] (but recommended) external ID for the offline user data job.
    -custom_key                     [optional] Only required after creating a custom key and custom values in the account. Custom key
                                    and values are used to segment store sales conversions. This measurement can be used to provide
                                    more advanced insights.
    -advertiser_upload_date_time    [optional] Date and time the advertiser uploaded data to the partner. Only required for third party uploads.
                                    The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. "2019-01-01 12:32:45-08:00".
    -bridge_map_version_id          [optional] Version of partner IDs to be used for uploads. Only required for third party uploads.
    -partner_id                     [optional] ID of the third party partner. Only required for third party uploads.
    -item_id                        [optional] A unique identifier of a product, either the Merchant Center Item ID or Global Trade Item Number (GTIN).
                                    Only required if uploading with item attributes.
    -merchant_center_account_id     [optional] A Merchant Center Account ID. Only required if uploading with item attributes.
    -country_code                   [optional] A two-letter country code of the location associated with the feed where your items are uploaded.
                                    Only required if uploading with item attributes.
                                    For a list of country codes see: https://developers.google.com/google-ads/api/reference/data/codes-formats#country-codes
    -language_code                  [optional] A two-letter language code of the language associated with the feed where your items are uploaded.
                                    Only required if uploading with item attributes.
                                    For a list of language codes see: https://developers.google.com/google-ads/api/reference/data/codes-formats#languages
    -quantity                       [optional] The number of items sold. Can only be set when at least one other item attribute has been provided.
                                    Only required if uploading with item attributes.
    -ad_personalization_consent		[optional] The ad personalization consent status.
	-ad_user_data_consent			[optional] The ad user data consent status.

=cut