[Из песочницы] Карты лояльности. Google Pay API for Passes в ASP.NET

Приложения-хранилища банковских карт стремительно вошли в нашу жизнь благодаря Apple Wallet и Google Pay. Обе платформы, помимо банковских, позволяют также работать и с другими типами карт — картами лояльности, подарочными картами, билетами на мероприятия, посадочными талонами и т.д.
 

gangrq7tlqwob2cmgxa1dydoasu.png

Работая в компании, которая занимается обслуживанием одной немаленькой ритейловой сети, мне пришлось интегрировать карты лояльности этой сети в Apple Wallet и Google Pay. И если с Apple Wallet пришлось повозиться только потому что интеграционный слой довольно многофункционален, то с Google Pay большая часть усилий и нервных клеток была потрачена на то чтобы разобраться в документации, найти подходящие инструменты и разработать первый proof of concept. Хоть в целом остальная часть работы прошла гораздо быстрее, чем для Apple Wallet, я потратил день на то чтобы разобраться в том как завести сервис, так что был бы не против, если бы кто-то написал подобную статью до меня.

1. Матчасть


Карты лояльности представлены при помощи двух сущностей — Loyalty Class и Loyalty Object.
 

  • Loyalty Class — своего рода шаблон для всех карт лояльности. Содержит поля общие для всех карт, такие как цвет шрифта, ссылки на ассеты иконок, бэкграундов, текстовые поля и т.д., полное описание можно найти тут: Loyaltyclass
  • Loyalty Object — экземпляр LoyaltyClass. Он же содержит в себе данные конкретной карты конкретного клиента — номер карты, имя, дополнительные текстовые поля. Описание тут: Loyaltyobject


Для того чтобы получить карту, пользователь должен перейти по ссылке формата
www.android.com/payapp/savetoandroidpay{JWT}, где JWT — токен, создержащий JSON с данными LoyaltyClass. Но так как длина URL имеет ограничения, рекомендуется предварительно создать Loyalty Class, если его структура содержащит много символов.

Если необходимо обновить карту в связи с изменением шаблона, стилей, необходимостью добавления или изменения текстовых полей, сделать это можно при помощи API. Необходимо только выполнить POST-запрос с новой версией карты, сервисы Google сами синхронизируют все приложения пользователей, у которых сохранена эта карта.


2. Инструменты и документация

Проблемой оказалось то, что все примеры в документации к Google Pay API for Passes были исключительно для Java, PHP и Python, а сама документация настойчиво рекомендовала «использовать клиентские библиотеки для того чтобы упростить процесс» работы с API.

Последовав этому совету я радостно отправился в nuget, но библиотеки для Google Pay там не оказалось. Слава Брину, первая строчка в гугле по запросу «google pay for passes dotnet» выдала страницу Google Pay API for Passes utility libraries, на которой нашлось то, что мне было нужно, правда в формате ZIP-архива, в котором находился .net проект со сгенерированным классом, являющимся враппером к API Google Pay — Google Pay API for Passes Client library.

Судя по наличию в проекте файла Google.Apis.Walletobjects.v1.1.9.2.00.nuspec, деплой nuget-пакета все же входил в планы команды Google. Открыв этот файл в поисках документации, я ничего конкретного не нашел, а некоторые ссылки, которые были в секции description и вовсе отправляли на несуществующие страницы.
 

3. Получение access token

Для начала непосредственной работы с Google Pay API for Passes необходимо получить access token, для этого же необходимо:
 

  1. Иметь Google Merchant Account, получить который можно здесь
  2. Cоздать Service Account, получить для него файл с credentials — json документ с деталями service account, включающий идентификатор, email, private key и т.д. Как рекомендует документация, хранить этот файл нужно в защищенном месте.
  3. Связать Merchant Account и Service Account в Google Merchant Center


При помощи этого файла можно авторизоваться, используя библиотеку Google.Apis.Auth.OAuth2:

private await Task GetOAuthToken()
{
         string serviceAccountFile = string.Empty;
         serviceAccountFile = ConfigurationManager.AppSettings["GooglePayServiceAccountConfigPath"];
        /*
                Библиотека авторизации имеет несколько типов Credential, GoogleCredential был выбран потому что его без проблем можно создать из файла с деталями Service Account, используя статический метод, при этом не теряются необходимые scopes API, указанные в этом файле
         */
         var credential = GoogleCredential.FromFile(serviceAccountFile)
                            .CreateScoped(WalletobjectsService.Scope.WalletObjectIssuer);

        /* 
                Access token имеет срок годности и должен быть обновлен вовремя, но метод GetAccessTokenForRequestAsync берет все действия по управлению валидностью токена на себя, так что об этом можно не волноваться
        */
         var token = async credential.UnderlyingCredential.GetAccessTokenForRequestAsync();
         return token;
}


4. Создание карты

Для того чтобы создать карту лояльности, нужно сперва создать Loyalty Class. Loyalty Class можно создать как при помощи API, так и используя веб-интерфейс Google Merchant Center. Внимание стоит уделить названию класса, так как это название должно быть уникальным в инфраструктуре Google Pay.

После создания Loyalty Class, можно создать Loyalty Object. Для этого уже понадобится библиотека, которую мы добавили в проект ранее: создаем объект запроса, указываем OAuth token, передаем созданный объект LoyaltyObject, выполняем запрос:

public GooglePayApiService()
{
     //Идентификатор эмиттера, выданный после создания Merchant Account
     IssuerId = ConfigurationManager.AppSettings["GooglePayIssuerId"];  
     _wobService = new WalletobjectsService();
}

private async Task ConvertLoyaltyObjectFromTemplate(GooglePayPassTemplate template)
 {
      string id = $"{IssuerId}.{template.SerialNumber}";

      string loyaltyClassName = ConfigurationManager.AppSettings["GooglePayStoreCardClassName"];
      var loyaltyClass = await GetLoyaltyClass(loyaltyClassName);

        
      var result =  new LoyaltyObject
      {
           Id = id,
           AccountName = template.AccountName,
           Barcode = new Barcode
           {            
                  AlternateText = template.BarcodeText,
                  Value = template.SerialNumber,
                  Type = "pdf417"
            },
            Kind = "walletObject#loyaltyObject",
            ClassId = loyaltyClass.Id,
            ClassReference = loyaltyClass,
            State = "active"
       };

       return result;
}

private async Task CreateLoyaltyObject(LoyaltyObject loyaltyObject)
{
      var saveRequest = _wobService.Loyaltyobject.Insert(loyaltyObject);
      saveRequest.OauthToken = await GetOAuthToken();

      var savedObject = await saveRequest.ExecuteAsync();

      return savedObject;
}


GooglePayPassTemplate в данном примере — это DTO, хранящий в себе шаблон карты для пользователя, который генерируется отдельным разработанным сервисом.

5. Обновление карты

Здесь принцип тот же, что и при создании: генерируем запрос, передаем обновленный объект LoyaltyObject, выполняем:

private async Task UpdateLoyaltyObject(LoyaltyObject loyaltyObject)
{
      var updateRequest = _wobService.Loyaltyobject.Update(loyaltyObject, loyaltyObject.Id);
                  updateRequest.OauthToken = await GetOAuthToken();
      var savedObject = await updateRequest.ExecuteAsync();
      return savedObject;
}

После выполнения запроса, карта в приложениях пользователей, у которых она установлена, будет обновлена спустя несколько секунд, если устройство в сети и есть подключение к Интернет.

6. Генерация JWT

Для установки карты необходимо перенаправить пользователя по ссылке www.android.com/payapp/savetoandroidpay{JWT}. Описание структуры токена можно найти по этой ссылке.

Токен подписывается RSA-SHA256 подписью, которую можно сгенерировать, используя все тот же файл с Service Account credentials:

public static class JwtHelper
{
        public static string CreateJwtForLoyaltyObject(LoyaltyObject loyaltyObject)
        {
            /*
                   В этот раз для работы с credential используем этот класс, потому что у него 
 статический метод создания подписи
            */
            ServiceAccountCredential credential;
            var now = DateTime.UtcNow;

            DateTime unixEpoch = new DateTime(1970, 01, 01); // 1970-01-01 00:00:00 UTC
            var secondsSinceEpoch = (int)Math.Round((now - unixEpoch).TotalSeconds);

            string serviceAccountFile = serviceAccountFile = ConfigurationManager.AppSettings["GooglePayServiceAccountConfigPath"];

            using (var fs = new FileStream(serviceAccountFile, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                credential = ServiceAccountCredential.FromServiceAccountData(fs);
            }
            /*
                   Создаем экземпляр класса JwtPayload, был создан для хранения payload-сегментa токена JWT
            */
            var jwtPayload = new JwtPayload
            {
                iat = secondsSinceEpoch,
                iss = credential.Id,
                payload = new JwtInternalPayload
                {
                    loyaltyObjects = new[]
                    {
                        new LoyaltyObjectPayload
                        {
                            id = loyaltyObject.Id
                        }
                    }
                }
            };

            string header = @"{""alg"":""RS256"",""typ"":""JWT""}";
            string payload = JsonConverter.SerializeObject(jwtPayload);

            string base64Header = EscapedBase64(Convert.ToBase64String(Encoding.UTF8.GetBytes(header)));
            string base64Payload = EscapedBase64(Convert.ToBase64String(Encoding.UTF8.GetBytes(payload)));

            //считаем саму подпись при помощи статического метода класса Signature
            string signature =
                EscapedBase64(credential.CreateSignature(
                    Encoding.UTF8.GetBytes($"{base64Header}.{base64Payload}")
                ));

            var token = $"{base64Header}.{base64Payload}.{signature}";

            return token;
        }

        private static string EscapedBase64(string base64)
        {
            return base64.Replace('+', '-')
                .Replace('/', '_')
                .Replace("=", "");
        }
    }



Заключение

В этой статье мы поверхностно прошли по основам работы с Google Pay API for Passes: настройке аккаунтов, подключению к API, созданию Loyalty Class и Loyalty Object.

Если НЛО благоволит, расскажу отдельно про то как работать с Apple Wallet (там все сложнее в плане реализации), как подружить Apple Wallet с Google Pay в одном веб-сервисе и не испытывать боли.


Полезные ссылки


© Habrahabr.ru