Publicar en Google Bussines, salir en maps

Un ejemplo de cómo implementar el código para gestionar ofertas y contenidos (local posts) en Google My Business (Google Business Profile) mediante su API. Este código se basa en la infraestructura y patrones sugeridos anteriormente: obtención de tokens a través de un servicio genérico (IExternalTokenService), uso de HttpClient para llamadas a la API y DTOs simples para modelar las respuestas y peticiones.

Importante:

  • Google My Business ahora se denomina Google Business Profile.
  • La API utilizada es la Google My Business API (o Google Business Profile API).
  • Necesitas habilitar la API en Google Cloud Console y contar con las credenciales OAuth 2.0.
  • Debes haber implementado el flujo OAuth y contar con un RefreshToken y AccessToken vigentes.
  • La creación de posts, ofertas y otro contenido requiere permisos específicos y scopes adecuados (por ejemplo, https://www.googleapis.com/auth/business.manage).

Documentación:
Consulta la documentación oficial para los endpoints, ya que puede cambiar con el tiempo. En la versión actual (v4) de la API, las localPosts se gestionan en:
POST https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts
Ref: LocalPosts Reference

Ejemplo de Creación de una Oferta (Local Post de tipo 'OFFER')

A continuación, presentamos un servicio de aplicación GoogleMyBusinessContentAppService con métodos para crear, listar y actualizar posts. Suponemos que ya tienes el accountId y el locationId de tu negocio, así como el accountIdentifier para obtener el token.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

// Interfaz genérica para obtener tokens
public interface IExternalTokenService
{
    Task<string> GetValidAccessTokenAsync(string providerName, string accountIdentifier);
}

// DTOs para Local Posts
public class LocalPost
{
    public string Name { get; set; } // "accounts/{accountId}/locations/{locationId}/localPosts/{postId}"
    public string LanguageCode { get; set; } = "en";
    public string Summary { get; set; }
    public string EventType { get; set; } // "OFFER", "STANDARD", "EVENT"
    public Offer Offer { get; set; }
    public List<MediaItem> Media { get; set; }
}

public class Offer
{
    public string CouponCode { get; set; }
    public string RedeemOnlineUrl { get; set; }
    public string TermsConditions { get; set; }
    public TimeRange OfferTime { get; set; }
}

public class TimeRange
{
    public string StartTime { get; set; } // RFC3339 format: "2024-01-01T09:00:00Z"
    public string EndTime { get; set; }   // RFC3339 format
}

public class MediaItem
{
    public string MediaFormat { get; set; } = "PHOTO"; // o "VIDEO"
    public string SourceUrl { get; set; }
}

public class LocalPostListResponse
{
    [JsonProperty("localPosts")]
    public List<LocalPost> LocalPosts { get; set; }
}

public class GoogleMyBusinessContentAppService
{
    private readonly IExternalTokenService _tokenService;
    private readonly HttpClient _httpClient;

    // Constructor, inyectar dependencias
    public GoogleMyBusinessContentAppService(IExternalTokenService tokenService, HttpClient httpClient)
    {
        _tokenService = tokenService;
        _httpClient = httpClient;
    }

    /// <summary>
    /// Crea una oferta (local post) en Google My Business.
    /// </summary>
    public async Task<LocalPost> CreateOfferPostAsync(string accountIdentifier, string accountId, string locationId,
        string summary, string couponCode, string redeemUrl, string terms, DateTime startTimeUtc, DateTime endTimeUtc, string imageUrl)
    {
        var accessToken = await _tokenService.GetValidAccessTokenAsync("Google", accountIdentifier);
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        var url = $"https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts";

        var post = new LocalPost
        {
            EventType = "OFFER",
            Summary = summary,
            Offer = new Offer
            {
                CouponCode = couponCode,
                RedeemOnlineUrl = redeemUrl,
                TermsConditions = terms,
                OfferTime = new TimeRange
                {
                    StartTime = startTimeUtc.ToString("o"),
                    EndTime = endTimeUtc.ToString("o")
                }
            },
            Media = new List<MediaItem>()
        };

        if (!string.IsNullOrEmpty(imageUrl))
        {
            post.Media.Add(new MediaItem { SourceUrl = imageUrl });
        }

        var content = new StringContent(JsonConvert.SerializeObject(post), System.Text.Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(url, content);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var createdPost = JsonConvert.DeserializeObject<LocalPost>(json);
        return createdPost;
    }

    /// <summary>
    /// Lista los posts (incluyendo ofertas) existentes en una ubicación
    /// </summary>
    public async Task<List<LocalPost>> ListPostsAsync(string accountIdentifier, string accountId, string locationId)
    {
        var accessToken = await _tokenService.GetValidAccessTokenAsync("Google", accountIdentifier);
        _httpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        var url = $"https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts";

        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var postsData = JsonConvert.DeserializeObject<LocalPostListResponse>(json);
        return postsData?.LocalPosts ?? new List<LocalPost>();
    }

    /// <summary>
    /// Actualiza un post existente. Por ejemplo, extender la fecha de la oferta o cambiar el texto.
    /// </summary>
    public async Task<LocalPost> UpdatePostAsync(string accountIdentifier, string postName, string newSummary)
    {
        var accessToken = await _tokenService.GetValidAccessTokenAsync("Google", accountIdentifier);
        _httpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        // postName viene con el formato "accounts/{accountId}/locations/{locationId}/localPosts/{postId}"
        var url = $"https://mybusiness.googleapis.com/v4/{postName}?updateMask=summary";

        var updatePayload = new { summary = newSummary };
        var content = new StringContent(JsonConvert.SerializeObject(updatePayload), System.Text.Encoding.UTF8, "application/json");
        var response = await _httpClient.PatchAsync(url, content); // PATCH no está en HttpClient por defecto, puedes simularlo con:
        // var req = new HttpRequestMessage(new HttpMethod("PATCH"), url) { Content = content };
        // var response = await _httpClient.SendAsync(req);

        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        var updatedPost = JsonConvert.DeserializeObject<LocalPost>(json);
        return updatedPost;
    }

    /// <summary>
    /// Elimina un post existente
    /// </summary>
    public async Task DeletePostAsync(string accountIdentifier, string postName)
    {
        var accessToken = await _tokenService.GetValidAccessTokenAsync("Google", accountIdentifier);
        _httpClient.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);

        var url = $"https://mybusiness.googleapis.com/v4/{postName}";
        var response = await _httpClient.DeleteAsync(url);
        response.EnsureSuccessStatusCode();
    }
}

// Extensión para HttpClient para realizar PATCH (si no tienes .NET 5+ o un método HttpMethod.Patch)
public static class HttpClientExtensions
{
    public static async Task<HttpResponseMessage> PatchAsync(this HttpClient client, string requestUri, HttpContent content)
    {
        var request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri)
        {
            Content = content
        };
        return await client.SendAsync(request);
    }
}

Uso del Servicio

Ejemplo de uso dentro de un ApplicationService mayor:

public class PromotionPublisherAppService
{
    private readonly GoogleMyBusinessContentAppService _gmbContentService;

    public PromotionPublisherAppService(GoogleMyBusinessContentAppService gmbContentService)
    {
        _gmbContentService = gmbContentService;
    }

    public async Task PublishSpecialOfferAsync(string accountIdentifier, string accountId, string locationId)
    {
        var summary = "Oferta especial: 2x1 en sushi";
        var couponCode = "SUSHI2024";
        var redeemUrl = "https://mi-restaurante.com/ofertas";
        var terms = "Válido hasta agotar existencias.";
        var startTime = DateTime.UtcNow;
        var endTime = DateTime.UtcNow.AddDays(7);
        var imageUrl = "https://mi-storage.blob.core.windows.net/promotions/sushi2x1.png";

        var createdPost = await _gmbContentService.CreateOfferPostAsync(
            accountIdentifier, accountId, locationId, summary, couponCode, redeemUrl, terms, startTime, endTime, imageUrl
        );

        Console.WriteLine($"Post creado: {createdPost.Name}");
    }
}

Consideraciones Finales

  • Ajusta los scopes OAuth y permisos necesarios en Google Cloud Console.
  • Asegúrate de tener el accountId y locationId correctos. Estos se obtienen llamando a accounts y locations endpoints previamente, como se mostró en ejemplos anteriores.
  • Valida las fechas y formato RFC3339 (.ToString("o")) para las ofertas.
  • Maneja errores y excepciones más robustamente en un entorno real.
  • Consulta la documentación actualizada de Google Business Profile para confirmar endpoints, campos y permisos.

Este ejemplo muestra la lógica base para crear, listar, actualizar y eliminar posts (ofertas) en Google My Business mediante la API, integrándolo con el flujo de autenticación previamente descrito y un servicio genérico para manejo de tokens.