[recovery mode] Интеграция Ultima 2C и Ebay. Личный опыт

Разработчик партнера согласился поделится личным опытом интеграции с EBay.


Мы постараемся пошагово разобрать весь процесс, укажем, на какие детали подключения стоит обратить особо тщательное внимание; какую базовую логику понадобится реализовать в обработчиках и сервисах вашей конфигурации для регулярного взаимодействия с EBAY. Ну, а также, осветим типичные трудности и неудобства, с которыми совершенно неизбежно столкнетесь при данной интеграции (и вы, и заказчики).



1.Особенности Ebay

В первую очередь, конечно, неприятные особенности работы Ebay, с которыми придется иметь дело не только разработчику:


  1. Ebay не любит РФ. Техническая поддержка — урезанная, некоторые функции в самом родном кабинете продавца Ebay могут периодически отваливаться. Подключают API неохотно.


  2. Конечно, не проблема торговать в качестве нероссийского продавца. И указать в настройках другую country of origin. Но понадобится реальное юрлицо (в моем случае специально регистрировали китайское, например), телефон с кодом указанной страны для подтверждения. PayPal, как основная система оплаты очень тщательно проверяет данные, особенно на юрлица и продавцов. Billing address и прочие данные должны быть реальным


  3. EBay вводит все больше лимитов на товары, которые можно постить бесплатно. Весь ассортимент уже не постим, выбираем что нужнее.

2.Merchant Integration Platform (MIP)

Что это такое можно почитать тут.


Но только вводную часть. Это талмудическое произведение не стоит подробного ознакомления. Понадобится технически очень мало. А помощи при багах и невнятных ошибках интеграции — никакой.


Кратко — это api –надстройка yад API Ebay. У последнего есть свое Trading Api и Shopping Api, но слишком громоздко, и сама поддержка EBAY заворачивает на MIP говоря что во избежание любой вашей конкретной проблемы надо использовать MIP.


MIP позволяет отправлять все данные на вход в аккаунт ибея (товары, наличие, цены, правила продавца) и получать все важное на выходе (собственно, заказы) через систему xml фидов.


2.1 Административные задачи


Оформить по всем правилам аккаунт селлера с юрлицом, привязать paypal, написать в поддержку MIP от своего аккаунта с просьбой MIP подключить к магазину.


2.2 Задача разработчика


  • Настроить тех данные в кабинете EBAY после подключения MIP.
  • Сохранить данные доступа в константы (или справочник, если планируется использование нескольких аккаунтов EBAY).
  • Сделать класс –генерилку входных фидов с нашими данными.
  • Сделать парсер входных данных.

3. Этапы подключения:

3.1 Платформа интеграции продавца


После того как MIP сообщит, что ваш магазин подключили, на сайте ebay в Moй Ebay → Краткий обзор → Учетная запись появится пункт меню «Платформа интеграции продавца».


3.2 Еще немного настроек:


  • зайти в Международные настройки,
  • заполнить адрес и прочее
  • Создать Правила

правила


Речь идет о правилах Оплаты, Доставки, Возврата.


При клике на них клиент отправляется в соответствующий пункт кабинета, где создает политики для каждого из этих разделов.


Важно!


  1. Кабинет дает создать сколько угодно политик. НО MIP принимает только 1 политику оплаты, 1 доставки, 1 возврата. Поэтому задача в настройках в рамках одной записи задать все возможные правила: Для Любого веса, Региона и так далее.


  2. Политике предлагается дать имя, ограничений на ввод нет. Но для MIP необходимо, чтобы политика называлась латиницей, без пробелов и подчеркиваний.

политика


3.3 Схема Ленты


Дальше заходите в «Схема Ленты» и выбираете формат (рекомендуется Ebay) и формат файла (мы предпочли XML). Там же можно скачать архив с примерами фидов (и структуры файлов на фтп сервере).


схема


3.4 Настройка FTP


Там мы получаем\генерим данные для доступа к FTP серверу MIP, куда мы будем грузить фиды с товарами и откуда будем забирать фиды с резервами.(Сервер, порт, юзернейм, пароль.)


Настройка завершена, дальше — к логике


3.5 Подготавливаем метаданные для хранения настроек у нас


Для всех полученных выше настроек по подключению к Мипу необходимо создать справочник.


В справочник предлагается сразу забить константы, указывающие ссылки на данные, которые будем посылать в MIP и использовать при вкачке резервов.


Справочник EbayAccountSetting
ID Long Not NULL,
Name string(256) Long Not NULL,
MipServerAddress string(256) Long Not NULL,
MipServerPort  Ineteger Not NULL,
MipServerLogin string(256) Long Not NULL,
MipServerPassword string(2048) Long Not NULL,
NotifyEmails string(256) Long Not NULL, ), - можно указать имейл разработчиков или ответственных, которые будут следить за ошибками
ShippingPolicy string(256) Long Not NULL,
ReturnPolicy string(256) Long Not NULL,
PaymentPolicy string(256) Long Not NULL,
IsSuborder Boolean Not NULL default true,
LangID Long Not NULL , Reference Dictionary Language , - язык, для создания резерва
FirmID Long Not NULL , Reference Dictionary Firm, - фирма, для создания резерва
OfficeReserveID Long Not NULL , Reference Dictionary Store, - офис резерва
StoreReserveID Long Not NULL , Reference Dictionary Store, - склад резерва
CurrencyID Long Not NULL , Reference Dictionary Currency, валюта резерва
PriceTypeID Long Not NULL , Reference Dictionary PriceType, - ценовая категория резерва
PriceZoneID Long Not NULL , Reference Dictionary PriceZone, - ценовая зона резерва
AgentGroupID Long Not NULL , Reference Dictionary AgentGroup, - папка КА
PaymentTypeID Long Not NULL , Reference Dictionary PaymentType - тип оплаты при создании резерва

Рекомендую добавить информационные поля, вроде ссылки на кабинет ebay, логин и пароль от сайта и так далее, чтобы при необходимости отладки не раскапывать эти данные по задачам.


В зависимости от нужд клиента и специфики учета документов продажи добавляем ссылки на другие справочники, определяющие свойства создаваемых резервов или выгружаемых товаров.


Также нам надо сделать просто таблицу в оракле, чтобы следить за тем, какие товары мы загрузили в ebay.


Нам надо будет снять эти товары из продажи, если в основном запросе (товары, которые на сегодняшний день попадают в выборку) их не будет (пропадет наличие или цена), а по-другому их будет вытаскивать муторно и неудобно.


CREATE TABLE EBAY_EXPORTED_GOODS
(
 EBAY_ACOUNT_ID  NUMBER(18) NOT NULL, - FK на нашу табличку EBAY_ACCOUNT_SETTINGS
 ARTICLE_ID  NUMBER(18) NOT NULL, - FK на нашу табличку ARTICLES
 UPLOAD_DT DATE
)

Также нам понадобится отправлять для каждого товара код его категории в системе EBAy, и чтобы менеджеры могли легко маппить наши товары к их категориям понадобится справочник категорий Ebay.


За основу мы можем взять наш справочник товарных категорий ArticleGroups. Новый справочник тоже будет древовидный.


EbayCategories
ID long PK
Name string – varchar(2048)
ParentID long (referential на самого себя)
Code – string (будем хранить код ebay отдельно от нашего PK чтобы скрипт SQL с генерацией нового ID не запутал нас и не сгенерил новые случайные айдишники)

Справочник должен быть древовидным по ParentID.


Наконец, в нашем базовом справочнике ArticleGroups сделаем поле EbayCode string.


В Форме редактирования наших родных категорий товаров, надо сделать так, чтобы менеджер мог выбрать подходящую категорию Ebay. После выбора, при сохранении нашей категории в поле


EbayCode нашего ArticleGroups сохраним Code выбранной записи из справочника Категории Ebay.


Далее, нам понадобится создавать Документы продажи, Клиентов, Адреса доставки, при импорте заказов.


Для этого в документ Sale добавляем EbayDocumentNo string (2048)
В CustomerAttribute добавляем EbayUserID string (256)
В DeliveryAddress EbayAddressID string (256)


4 Интеграция

4.1 Система фидов


Если мы посмотрим на админку Мипа в кабинете продавца, зайдем на фтп сервер с учетными данными, полученными в 3.4, а также глянем на пример фидов, скачанный в 3.3, то увидим одинаковую структуру
фиды1
фиды2


Фиды распределены по папкам. В папках лежат одноименные фиды (Availability.xml, Distribution.xml…)


3 фида мы должны отдать ebay, сообщив о наших товарах, их наличии и ценах (Availability, Distribution, Product)


1 фид мы должны вкачать — Order


Фиды, которые мы выкачиваем от нас к EBAY, кладутся нами в корень соответствующей папки. Все. Дальше Ebay сам ставит в очередь, перекидывает в папку обработки, потом в папку архива, выдает лог.


Фид, который мы закачиваем от EBAY к нам мы тоже берем из корня order. Редактировать, удалять или делать что-то еще с файлом не надо.


Предусмотрите, какой класс для записи, а главное, Парсинга XML будет максимально удобным.


Желательна максимальная гибкость, возможность не идти по тегам, а перескакивать по элементам и выхватывать их по enumerаtorам, благо большая часть фида будет шлаком, который импортировать нет необходимости.


Мне было достаточно System.Xml.Linq.XDocument


За передачу данных от нас Ebay«ю отвечают три фида:


4.1.1 Product


Описательная инфа по товарам. Название, атрибуты (характеристики), категория.


Все обязательные теги смотрим в скачанном примере.


          

                

                   369866

                                

                                    LCD cable for Acer for Aspire 3820, 3820T, 3820G, 3820TG, 3820TZ

                                                

                                                                LCD cable for Acer for Aspire 3820, 3820T, 3820G, 3820TG, 3820TZ

                                                

                                                http://oursite.com/good_big_pics/369866.jpg

                                                168061 

                                                

                                                                New

                                                                Brand new

                                                

                                

                


Замечания:


-New

Brand new

Можно считать константой. Разобраться лично нам с ограничениями по постингу б/у товаров не удалось — Названия состояний Enumerable, которые фиг раскопаешь в документации. Товар должен быть new.


Все теги с атрибутом localizedFor=«en_US» подразумевают, что для одного товара можно разместить разную информацию для разных подсайтов ebay, соответствующих указанной локали (например, разные названия для En и Ru)


4.1.2 Availability


Передает только наличие в штуках.



    
        369866
        5
    

4.1.3 Distribution


Цены, лимиты покупки, ПОЛИТИКИ, о которых мы говорили в 3.2
Тут как раз понадобятся англ. Названия Доставки, Возврата и Оплаты.



  BaseShipping 
 5
 Base 
  BaseReturn 
    
        7.58
    

Валюта тут не указывается, она указывается клиентом в личном кабинете.


4.1.4 Взаимосвязь фидов


Правила постинга фидов очень просты


  1. Вы сами задаете периодичность, с которой они постятся в Ebay, лимитов нет. Соответственно таск, который будет генерировать фиды, настраиваете сообразно графику работы других тасков, обновляющих существенные параметры товаров (наличие, цены). Слишком часто его дергать не следует, благо у MIP своя очередь, и порой объемный фид может стоять в очереди по пару часов.


  2. Уникальным идентификатором товара в 3х фидах служит тег SKU, соответственно лучше не оригинальничать, и не добавлять туда сложные конструкции с партномерами, а постить что-то столь же однозначное и уникальное — наш артикул товара.


  3. Фиды «воспринимаются» по очереди. Сначала должен уйти на фтп Product, потом Availabiliy, потом Distribution.


  4. Снятие товаров производится отсылкой наличия = 0.
    Поэтому заблаговременно предусмотрите таблицу, в которой будете хранить товары, которые уже запостили в MIP


  5. Каждый xml пакуется в zip и кладется на фтп, в одноименную папку. Предусмотрите библиотеку для работы с фтп.
    Мы брали Renci.SshNet.SftpClient
    Пример метода загрузки файла этой библиотекой


    private void Upload(long accountId, string zipFileName, string innerPath)
    {
    var EbayAccount = DictionaryManager.GetRecord(accountId);
    var serverAdr = EbayAccount.MipServerAddress; //mip.ebay.com;
    var port = (int)EbayAccount.MipServerPort;// 22
    var login  = EbayAccount.MipServerLogin; // my acc
    var password = EbayAccount.MipServerPassword;           
    
    using(var fStream = new System.IO.FileStream(zipFileName, System.IO.FileMode.Open))
    {
      using(var c = new Renci.SshNet.SftpClient(serverAdr, port, login, password))
      {
            c.Connect();
            c.OperationTimeout = new TimeSpan(0, 15, 0);
            var fullPath = innerPath +System.IO.Path.GetFileName(zipFileName);
            /*innerPath – путь в структуре данных на мип сервере. Например, store/availability. zipFileName –  ваш локальный путь к сегенереному вами файлу с данными*/
            if (c.Exists(fullPath))
            {
                 try
                 {
                      c.DeleteFile(fullPath);
                 }
                 catch (Exception e)
                 {
                      LogManager.GetLogger().Info(@"Экспорт ebay files to mip. error delete exist file. fileName = {0}; error = {1}", fullPath, e.Message);
                 }
            }
            c.UploadFile(fStream, fullPath, true);
      }
    }
    } 

  6. спустя время (зависящее от очереди ebay) результат обработки MIP кладет на фтп в ту же папку, в подпапку
    Output/{current_date: M-dd-YYYY}/…. .xml

Например:


Мы загружаем наш файл с остатками на FTP
  В папку /store/availability
Результат выплюнется в /store/availability/output/Jul-18-2016/

  1. Также можно параллельно грузить или просто смотреть результаты обработки фидов в кабинете в Архиве лент


  2. В 99% случаев, если и была какая-то ошибка, на этапе Product.xml и Availability.xml MIP промолчит и скажет, что все ок. Ошибка будет, разве что у вас откровенно malformed xml.

Даже если ошибки связаны с этими фидами, ошибки будут видны только в результате обработки последнего фида, Distribution.xml


  1. Вопрос о том, имеет ли смысл парсить результаты выгрузки к нам в ерп — ну крайне спорный.
    Никакой системности, сериализуемости в ошибках нет.

Более того, по процентам 70 из них приходится обращаться письменно в поддержку MIP, что само по себе мероприятие с 50% результативностью. Запросы часто приходится высылать несколько раз, так как успешность обращения зависит от сотрудника, на которого попадешь.


Гугл ошибок помогает мало.


Также, чрезвычайно часто выясняется, что просто у самого Mip был какой-то issue, о тех. подробностях которого они не распространяются, просто сообщая, что Пользуйтесь на здоровье, мы пофиксили.


Также, наличие в ответе ошибки не гарантирует, что все остальные товары не прогрузились.


Иногда одна ошибка блокирует все товары, даже те, к которым она не относится, иногда только из всего фида.


Суммируя, можно сказать, что имеет смысл просто следить за результатами выгрузки в кабинете.


Ради тавтологии можно остановиться на таком решении:


Парсить по датам содержимое папки /output/distribution. В Xml захватывать только теги

И в текстово-информативно-списочном варианте просто дублировать их в отдельный справочник в ерп с датами, есл клиент хочет смотреть ошибки в ерп, а не в кабинете.


Пример дефектного ответа




  
    6124
    EBAY_RU
    FAILURE
    
      API_95
      ERROR
      REQUEST
      
    
  
  FAILURE

Итого, попробуем сгенерировать и залить последовательно 3 фида.


var startDate = DateTime.Now; //Поставим дату начала выгрузки, чтобы пометить ей товары, попавшие в текущую выборку
var accountId = 1L; //Выбирам аккаунт
var account = DictionaryManager.GetRecord(accountId); //Данныепо аккаунту
var productDoc  = ExportXmlProducts(account, startDate); //Сформируем xml с товарами
ZipAndUploadFile(accountId, productDoc, "Product", "/store/product/"); //Зальем
var availDoc  = ExportXmlAvailability(account, startDate); //Сформируем xml с остатками
ZipAndUploadFile(accountId, availDoc, "Availability", "/store/availability/"); //Зальем
var distrDoc  = ExportXmlAvailability(account, startDate); //Сформируем xml с ценами и правилами
ZipAndUploadFile(accountId, availDoc, "Distribution", "/store/distribution/"); //Зальем

Формируем xml Товаров Products.xml


private XDocument ExportXmlProducts(EbayAccountSetting account, DateTime startDate)
{
   var listings = new XElement("productRequest");
   //Получим данные по товарам, при этом возьмем только товары с ценой, остатками и категорией Ebay
   var sql = @"  SELECT A.ID,
                 NVL(PT.STRING_VALUE, A.ONLINE_NAME) AS NAME,
                        P.VALUE PRICE,
                        CASE WHEN PH.ARTICLE_ID IS NOT NULL
                        THEN PH.ARTICLE_ID||'_'||PH.VIEW_ID
                        ELSE ''
                        END  ICON_ID,
                        A.FEATURE_SET_ID,
                        B.NAME,
                        AG.EBAY_CODE EBAY_CODE
                        FROM ARTICLES A
                        JOIN /* Получим только те что на нашем складе */  VTB_STOCK VB ON VB.ARTICLE_ID=A.ID and  VB.STORE_ID =:vStoreID
                        JOIN PRICES P ON A.ID=P.ARTICLE_ID AND P.PRICE_TYPE_ID=:vPriceTypeD AND P.PRICE_ZONE_ID=:vPriceZoneID /* Цена по нашей категории и зоне из аккаунта*/
                      JOIN BRANDS B ON A.BRAND_ID=B.ID
                      JOIN ARTICLE_GROUPS AG ON A.GROUP_ID=AG.ID
                      LEFT    JOIN ARTICLE_PHOTOS PH ON A.ID=PH.ARTICLE_ID AND PH.PHOTO_ORDER=1
                      LEFT JOIN KERNEL.PROP_TRANSLATIONS PT ON PT.VALUE_ID = a.ID AND PT.OBJECT_ID = :vObjectID AND PT.LANG_ID = :vLangID
                   WHERE VB.QUANTITY>VB.RESERVE_QUANTITY /* Котрых больше нуля */
                         AND P.VALUE>0 /* Цена больше нуля */
                         AND AG.EBAY_CODE IS NOT NULL /* Проставлена категория Ebay */ ";
    var pars = new Dictionary{ {"vObjectID", productObjectId},
                                                        {"vLangID", account.LangID},
                                                        {"vStoreID", account.StoreReserveID},
                                                        {"vPriceTypeD", account.PriceTypeID},
                                                       {"vPriceZoneID", account.PriceZoneID}};
    var goods = SqlService.Select(sql, pars);
    //Формируем xml товара одной структурой
    foreach (var good in goods)
    {
       var propNode  = GetProperties(Convert.ToInt64(good["ID"]), account.LangID);//Получим отдельно список хар-к
       var productNode = new XElement("product",
                        new XElement("SKU", good["ID"].ToString()),
                        new XElement("productInformation", new XAttribute("localizedFor", "en_US"),
                        new XElement("title", good["NAME"].ToString()),
                        new XElement("description",
                        new XElement("productDescription", good["NAME"].ToString())) ,
                        "",//propNode,
                        new XElement("pictureUrl",
                        String.Format(@"http://mysite.com/good_pics/{0}.jpg", good["ICON_ID"].ToString())),
                        new XElement("category", new XAttribute("Type", "eBayLeafCategory"), good["EBAY_CODE"].ToString()),
                        new XElement("conditionInfo",
                        new XElement("Condition", "new"),
                        new XElement("conditionDescription", @"Brand new")))
                );
        listings.Add(productNode);
        AddToExported(Convert.ToInt64(good["ID"]), account.ID, startDate);
     }
     var doc = new XDocument(listings);
     return doc;
}

Пара важных замечаний:


  • Если размещаете товары не на рускоязычном сайте, не забудьте забрать нужные переводы свойств.
  • КОДЫ ОБЪЕКТОВ МЕТАДАННЫХ, КОТОРЫЕ ПОДЛЕЖАТ ПЕРЕВОДУ, ЛУЧШЕ ВЫНЕСТИ В КОНСТАНТЫ, ОНИ ОДИНАКОВЫ И ИНВАРИАНТНЫ ДЛЯ ЯДРА.
    const long productObjectId = 2952;
    const long valueObjectId = 3485;
    const long featureObjectId = 3458;
    const long unitObjectId= 7680;

Нам надо отправить характеристики товара. Вспоминаем структуру шаблонов описания и характеристик. Наша задача привести все характеристики товара к виду ХАРАКТЕРИСТИКА (string) — ЗНАЧЕНИЕ (string), даже в тех случаях, когда идет речь о нескольких значениях для одной характеристики.


private string GetProperties(long articleId, long langId)
{
    var sql = @"WITH T AS (
                 WITH T AS (SELECT
                  F.ID AS PROP_ID,
            NVL(PT.STRING_VALUE, F.NAME) AS PROP_NAME,
           V.SORT_ORDER,
           CASE WHEN F.TYPE_ID=1 THEN TO_CHAR(D.NUMBER_VALUE)
            WHEN F.TYPE_ID=2 THEN TO_CHAR(D.BOOLEAN_VALUE)
            WHEN F.TYPE_ID=3 THEN (D.STRING_VALUE)
            WHEN F.TYPE_ID=4 THEN TEXT_VALUE
            WHEN F.TYPE_ID in (5,6,7) THEN nvl(PT1.STRING_VALUE,V.VALUE)
            END VAL,
            CASE WHEN UN.ID IS NOT NULL THEN
           ' '|| NVL(PT2.STRING_VALUE, UN.NAME)
           ELSE
           ''
           END  AS UNIT_NAME
            FROM ARTICLE_FEATURE_VALUES  D
            JOIN ARTICLE_FEATURES F ON D.FEATURE_ID=F.ID
            LEFT JOIN ARTICLE_FEATURE_UNITS UN ON F.UNIT_MEASUREMENT_ID=UN.ID
            LEFT JOIN ARTICLE_FEATURE_VALID_VALUES V ON D.VALUE_ID=V.ID
            LEFT JOIN KERNEL.PROP_TRANSLATIONS PT ON PT.VALUE_ID = F.ID AND PT.OBJECT_ID = :vPropObjectID AND PT.LANG_ID = :vLangID
            LEFT JOIN KERNEL.PROP_TRANSLATIONS PT1 ON PT1.VALUE_ID = V.ID AND PT1.OBJECT_ID = :vValObjectID AND PT1.LANG_ID = :vLangID
             LEFT JOIN KERNEL.PROP_TRANSLATIONS PT2 ON PT2.VALUE_ID = UN.ID AND PT2.OBJECT_ID = :vUnitObjectID AND PT2.LANG_ID = :vLangID
            WHERE D.ARTICLE_ID=:vProdID
           )
            SELECT PROP_ID, PROP_NAME, LISTAGG(VAL||UNIT_NAME, ',')WITHIN GROUP (ORDER BY SORT_ORDER) VAL
            FROM T
            GROUP BY  PROP_ID, PROP_NAME";

   var pars = new Dictionary{ {"vLangID", langId},
                                                    {"vProdID", articleId},
                                                    {"vValObjectID", valueObjectId},
                                                    {"vUnitObjectID", unitObjectId},
                                                    {"vPropObjectID", featureObjectId} };
   var features = SqlService.Select(sql, pars);
   var featuresqueue = features.Select(r => new XElement("attribute", new XAttribute("name", (string)r["PROP_NAME"]), (string)r["VAL"]).ToString())
            .ToList();
    return string.Join("", featuresqueue);
}

Сформируем файл остатков Availability.xml


private XDocument ExportXmlAvailability(EbayAccountSetting account, DateTime startDate)
{
   var listings = new XElement("inventoryRequest");
   //Получим все сохраненные нами экспортированные товары по этому акку. Если по дате не попали в последнюю выгрузку - снимаем с продажи - отправляем наличие = 0
   var sql = @"  SELECT EG.aRTICLE_ID ID, case when last_export_date < :vDat then NVL(VB.QUANTITY, 0)  - NVL(VB.RESERVE_QUANTITY, 0) else 0 end QUANTITY
FROM EBAY_EXPORTED_GOODS EG
LEFT JOIN VTB_STOCK VB ON VB.ARTICLE_ID=EG.ARTICLE_ID and  VB.STORE_ID =:vStoreID
WHERE EG.EBAY_ACCOUNT_ID=:vId ";
   var pars = new Dictionary{ {"vId", account.ID},
                                              {"vStoreID", account.ReserveStoreID}};
   var goods = SqlService.Select(sql, pars);
   foreach (var good in goods)
   {
      var productNode = new XElement("inventory",
                        new XElement("SKU", good["ID"].ToString()),
                        new XElement("totalShipToHomeQuantity", good["QUANTITY"].ToString()));
      listings.Add(productNode);
   }
   var doc = new XDocument(listings);
   return doc;
}

По тому же принципу формируем Distribution.xml


Пакуем наш xml в zip и аплоадим уже описанным нами методом


private void ZipAndUploadFile(long accountId, XDocument doc, string fileName, string innerPath)
{
    var tempDirectory = @"C:\WINDOWS\Temp\EbayTemp";//Путь для временных ахивов на сервере. Выделите папку, там же будем смотреть историю
    System.IO.Directory.CreateDirectory(tempDirectory);
    var cFullFileName = System.IO.Path.Combine(tempDirectory, fileName);
    var xmlFileName = System.IO.Path.ChangeExtension(cFullFileName, "xml");
    doc.Save(xmlFileName);
    var zipFullFileName = System.IO.Path.ChangeExtension(cFullFileName, "zip");
    var zipper = new ZipBuilder();
    zipper.AddFile(xmlFileName, delete: false);
    zipper.SaveArchive(zipFullFileName);
    Upload(accountId, zipFullFileName, innerPath);
}

За передачу заказов от Ebay к нам отвечает 1 фид.


4.1.5 Order


Заказы падают в папку order/output


Тут также файлы сгруппированы по датам
{date: M-dd-YYYY}/…. .xml
Например /store/order/output/Jul-10–2016/


Можно парсить папку output по датам.


Но для удобства MIP скидывает последний фид ответа в файлик /store/order/output/order-latest


Причем недавний заказ не перетирается новыми, а держится там определенное время. Так что даже несмотря на возможность фейла тасков у текущих клиентов было достаточно регулярно (раз в 5–10 минут) парсить именно этот файл.


Лайфхак:


Для удобства тестирования можно логику из таска вынести в сервис и создать пользовательскую команду, и для теста сделать возможность передачи на вход параметром файла xml. Тогда для тестирования вы можете скачать документ в кабинете ибея или по фтп и прогнать его несколько раз.


В шапку документа продажи надо добавить поле Номер EBAY. При обработке заказа проверяем, что заказов с таким номером нет (иначе просто игонрируем).


И последнее — все заказы фида уже оплачены. Схема расчетов в данном случае эквивалентна эквайрингу, где банком выступает paypal. Соответственно, после создания заказа необходимо создать соответствующие документы эквайринга (в базовой конфигурации через методы PaymentService).


Фид заказа содержит следующую важную информацию


  1. Инфа о клиенте: Фио, имейл, телефон


  2. Адрес, подробно, включая zip


  3. Список товаров с уникальным идентификатором, который вы передавали в фидах 1–3 в теге SKU


  4. Цену товаров
    Не забывайте, что никак нельзя получать в момент импорта актуальную цену по колонке, потому что неизвестно какой была цена того фида, по которому клиент увидел и купил этот товар. Ее берем из Ebay.


  5. Цену доставки
    Итого алгоритм импорта должен выглядеть грубо так:
    • Берем номер заказа Ebay. Проверяем по нему не импортировали ли уже его.
    • Делаем GetOrCreateAgent — по данным (#Фио, имейл, телефон#)
    • Делаем у агента GetOrCreateDeliveryAddress
    • Создаем резерв, заполняем тч товаров с нужными ценами.
    • Активируем тч доставки, проставляем код адреса и цену из Ebay.

Пример фида




  
    
      222034713176-1766499808012
      EBAY_US
      2016-07-20T12:45:09.000Z
      
        andreyka.mosin.1997-3
        Андрей
        Мосин
        andreyka.mosin.1997@mail.ru
      
      
        pdirect.ru
      
      
        
          
            
            RU_ExpeditedMoscowOnly
          
          
            4504234220015
            Андрей Мосин
            9527720655
            курчатова 16
            
            саров
            
           607183
            RU
          
          
        
      
      
        222034713176-1766499808012
                  CustomCode
          222034713176
          413480
          ELM327 BlueTooth V1.5 BIG автосканер ELM327 BlueTooth V1.5 BIG chip pic18f25k80
        
        1
        990.0
        
          
            990.00
          
          
            350.00
          
          
            SalesTax
            0.00
          
          
            ElectronicWasteRecyclingFee
            0.00
          
          1340.00
        
      
      
        3TD678480B805680J
        1340.0
      
      
        
          990.0
        
        
          0.0
        
        
          350.0
        
        
          0.0
        
        1340.0
      
      
        PAID
        NOT_SHIPPED
      
      
    
  

Примеры импорта


var accountId = 1L; //Выбирам аккаунт
var EbayAccount = DictionaryManager.GetRecord(accountId); //Получим данные чтобы передать дальше
var doc = Download(accountId); //скачаем Renci
foreach (var ordXml in  doc.Root.Elements("pendingOrderFulfillment"))
{
   if (ordXml.Elements("order").Count() == 0)
   {
       continue;
   }
   CreateReserve(ordXml.Elements("order").First(), EbayAccount); //Создадим резерв
}

Скачку организуем также, как и закачивали свой фид в Ebay


private XDocument Download(long accountId)
{
  var EbayAccount = DictionaryManager.GetRecord(accountId);
  var serverAdr = EbayAccount.MipServerAddress; //mip.ebay.com;
  var port = (int)EbayAccount.MipServerPort;// 22
  var login  = EbayAccount.MipServerLogin; // my acc
  var password = EbayAccount.MipServerPassword; 

  var doc = new XDocument();
  using(var c = new Renci.SshNet.SftpClient(serverAdr, port, login, password))
  {
       c.Connect();
       c.OperationTimeout = new TimeSpan(0, 15, 0);
       byte[] fileBytes = c.ReadAllBytes(@"/store/order/output/order-latest");
       if ( fileBytes == null || fileBytes.Length == 0)
       {
          return doc;
       }
       using (var stream = new System.IO.MemoryStream(fileBytes, false))
       {
          try
          {
             doc = XDocument.Load(new System.Xml.XmlTextReader(stream));
          }
          catch(Exception e)
          {
                LogManager.GetLogger().Info(@"Import ebay files. error  = {0}", e.Message);
          }
       }
   }

   return doc;
}

Собственно создание документа продажи.


private void CreateReserve(XElement ebayOrdXml, EbayAccountSetting EbayAccount)
{
    var ebayOrderID  = ebayOrdXml.Elements("orderID").First().Value;
    if (CheckOrderExists(ebayOrderID)) //Проверим по коду Ebay нет ли такого уже в базе. Они не апдейтятся. Если есть - пропускаем.
    {
         LogManager.GetLogger().Info(@"Заказ уже существует");
         return;

    }

    var shippingXml = ebayOrdXml.Elements("logisticsPlan").First().Elements("shipping").First().Elements("shipToAddress").First();
    var delivAmountFromEbay  = 0M;
    //Получим цену доставки. Считать ничего не надо, так как нам надо зафиксировать цену по которой уже купили и оплатили.
    delivAmountFromEbay = Decimal.Parse(ebayOrdXml.Elements("total").First().Elements("priceline").First().Elements("amount").First().Value.Trim(), System.Globalization.NumberStyles.AllowDecimalPoint, System.Globalization.CultureInfo.InvariantCulture);
    //Вытащим все что нам пригодится из тегов
    var phone  = shippingXml.Elements("phone").First().Value;
    var firstName = shippingXml.Elements("firstName").First().Value;
    var lastName = shippingXml.Elements("lastName").First().Value;
    var Street1 = shippingXml.Elements("addressLine1").First().Value;

    var Street2 = shippingXml.Elements("addressLine2").First().Value;
    var CityName = shippingXml.Elements("city").First().Value;

    var StateOrProvince = shippingXml.Elements("stateOrProvince").First().Value;
    var PostalCode = shippingXml.Elements("postalCode").First().Value;
    var EbayAddressID = shippingXml.Elements("addressID").First().Value;
    var ebayBuyerUserID = ebayOrdXml.Elements("buyerID").First().Value.Trim();
    var email =ebayOrdXml.Elements("buyer").First().Elements("email").First().Value;
    //Создадим или получим КА
    var agentID = CreateAgent(firstName, lastName, email,  phone,Street1,ebayBuyerUserID, EbayAccount);
    //Создадим или получим Адрес
    var adressID = CreateAgentAddress(agentID,  PostalCode, firstName,  phone, CityName, StateOrProvince, EbayAddressID,  Street1, Street2);
    //Все данные получили, создаем резерв
    var initialSubtype = SaleDocument.Subtypes.Reserve;
    var document = DocumentManager.NewDocument(initialSubtype);
    document.AgentID = agentID;
    document.OfficeID = EbayAccount.OfficeReserveID;
    document.PriceTypeID = EbayAccount.PriceTypeID;
    document.FirmID = EbayAccount.FirmID;
    document.StoreID = EbayAccount.StoreReserveID;
    //Основные заголовки есть, импортим товары, по SKU
    foreach(var transXml in ebayOrdXml.Elements("lineItem"))
    {
        var articleNo= Convert.ToInt64(transXml.Elements("listing").First().Elements("SKU").First().Value);
        var qty= Convert.ToInt64(transXml.Elements("quantity").First().Value);
        //Получим цену доставки. Считать ничего не надо, так как нам надо зафиксировать цену по которой уже купили и оплатили.
        var price  = Decimal.Parse(transXml.Elements("unitPrice").First().Value.Replace(".", ","));
        document.Articles.Add(new SaleArticleTablePartRow {
                    ArticleID = articleNo,
                    SaleQuantity = qty,
                    ReservedQuantity = qty,
                    OriginalPrice = price,
                    SalePrice = price,
                    Amount = price * qty
                });
     }
     //Воспользуемся очень удобной функой расчета доставки для вебсервиса и сайтов. У нас будет доставка outsource. Сумму м уже получили
     var deliveryWizardRow = WebService.GetDeliveryWizardRow(document, adressID, AgentType.Constants.CustomerID, DateTime.Now, "outsource", EbayAccount.ID, delivAmountFromEbay);
     document.DeliveryWizard.Clear();
     document.DeliveryWizard.Add(deliveryWizardRow);
     document.Delivery.AddRange(DeliveryService.CreateDeliveries(document));
     DocumentManager.SaveDocument(document);
}

Проверяем уникальность заказа по коду Ebay


private bool CheckOrderExists(string ebayOrderID)
{
    return (from d in DataContext.GetTable().Where(x => x.EbayDocumentNo == ebayOrderID) select d).Any();
}

Агента и адрес мы создаем ИЛИ апдейтим. Пробуем сначала получить запись по уникальному идентификатору Ebay, и только если нет — создать


private long CreateAgent(string firstName, string lastName, string  email, string phone, string Street1, string ebayBuyerUserID, EbayAccountSetting EbayAccount)
{
   var customer = new CustomerAttribute() {};
   var privatePerson = new PrivatePersonAttribute() {};
   var agent = new Agent() {};
   var exAgents = (from d in DataContext.GetTable().Where(x => x.Customer.EbayUserID == ebayBuyerUserID)

                            select new {
                                Id = d.ID,
                                CustomerID = d.CustomerID,
                                PrivatePersonID = d.PrivatePersonID
                            }).ToList();

   if (exAgents.Any())
   {
      customer = DictionaryManager.GetRecord(exAgents.First().CustomerID.GetValueOrDefault());
      privatePerson = DictionaryManager.GetRecord(exAgents.First().PrivatePersonID.GetValueOrDefault());
      agent = DictionaryManager.GetRecord(exAgents.First().Id);
    }
    else
    {
       customer = DictionaryManager.NewRecord(new CustomerAttribute
      {
                    EbayUserID = ebayBuyerUserID,
                    PriceTypeID = EbayAccount.PriceTypeID //Уник. ид. Ибея
       });

       privatePerson = DictionaryManager.NewRecord(new PrivatePersonAttribute
                {
                    GenderID = Gender.Constants.Undisclosed,
                });

                agent = DictionaryManager.NewRecord(new Agent
                {
                    TypeID = AgentType.Constants.CustomerID,
                    FormID = AgentForm.Constants.PrivatePersonID,
                    GroupID = EbayAccount.AgentGroupID
                });
     }
     customer.ReserveLifetime = 1;
     DictionaryManager.SaveRecord(customer);

     privatePerson.FirstName = firstName;
     privatePerson.LastName = lastName;
     privatePerson.Phone =phone;
     privatePerson.Email = email;

     DictionaryManager.SaveRecord(privatePerson);
     agent.Name = string.Format("{0} {1}", lastName, firstName);
     agent.PrivatePersonID = privatePerson.ID;
     agent.CustomerID = customer.ID;
     DictionaryManager.SaveRecord(agent);
     return agent.ID;
}

private long  CreateAgentAddress(long agentID,  string PostalCode,  string firstName, string phone, string CityName, string StateOrProvince, string EbayAddressID,  string Street1, string Street2)
{
    var address = new DeliveryAddress() {};
    var exAddress = DictionaryManager.GetRecords(x => x. EbayAddressID == EbayAddressID);//поищем по Уник. ид. ebay
    if (exAddress.Any())
    {
        address = exAddress.First();
    }
    else
    {
        address = DictionaryManager.NewRecord(new DeliveryAddress
         {
                    //Заполнить константами. МЫ не всегда сможем получить эти данные
                    DeliveryAreaID = DeliveryArea.Constants.Default,
                    Latitude = 0M,
                    Longitude = 0M,
                    EbayAddressID = EbayAddressID
                });
         }
         address.Address = String.Format("{1}, {2}, {3}, {4}", PostalCode, CityName, StateOrProvince, Street1, Street2);
         if (address.Latitude == 0)
         {
            //Попробуем координаты Геосервисом
            var locations  = GeoService.GetLocations(address.Address);
            if (locations.Any())
            {
                 address.Latitude = locations.First().Latitude;
                 address.Longitude = locations.First().Longitude;
                 address.DeliveryAreaID =  DeliveryService.FindDeliveryArea(address.Latitude, address.Longitude);
             }
          }
          address.ContactPersonName = firstName;
          address.ContactPersonPhones = phone;
          DictionaryManager.SaveRecord(address);
          return address.ID;
    }

5.Категории Ebay

У Ebay есть собственная структура категорий товаров. Наблюдать ее мы можем на
http://www.ebay.com/sch/allcategories/all-categories


Для всех локалей структура одинаковая, имеются только переводы названий, если мы зайдем на русский Ebay, но структура дерева и код категорий инварианта.


MIP требует указания принадлежности товара к той или иной категории, в Products.xml есть обязательный тег , причем сам Мип не предоставляет какого-либо интерфейса или метода Апи для получения списка этих категорий, предполагая, что интеграторы тут справятся сами).

Существует не очень удобная и запутанная система включения автоматического определения категории Мипом (по его собственным маркетинговым рекомендациям);, но мы ее не будем рассматривать, так как опыт показывает, что для точного позиционирования товара лучше нам иметь на руках весь список категорий, а вернее предоставить его контентщикам, и дать им самим настроить привязки.


Для этого нам понадобится:


  • создать у себя древовидный справочник для хранения категорий Ebay (описан в 3.5)


  • добавить ссылку на категорию Ebay у нашего справочника категорий товаров (описан в 3.5)


  • навесить проверку заполненности поля при сохранении нашей категории


  • собственно вкачать к себе структура категорий.

5.1 Импорт категорий из Ebay.


Обновлять дерево категорий придется крайне редко. Ebay заблаговременно вывешивает продавцам напоминание о грядущих изменениях в структуре категорий. Лучше сделать пользовательскую команду, которой менеджеры смогут сами перезаливать список категорий по мере надобности.


Вы в принципе можете написать парсер HTML, который будет ходить по странице http://www.ebay.com/sch/allcategories/all-categories и ее ссылкам и забирать категории, но в нашем случае рассмотрим возможность получения категорий через родное Api Ebay.


Это отдельное Api, надстройкой над которым служит MIP, о котором мы говорим в данной статье. Может показаться обидным подключать его только для списка категорий, но Настоятельно не рекомендуется пытаться через него работать с товарами и заказами. Особенно если речь и русскоязычном сайте — поддержка Ebay вас сразу завернет на Mip.


5.2. Подключение API Ebay.


Для подключения к Api Ebay вам понадобится зарегистрироваться как Ebay Developer. https://developer.ebay.com/base/membership/signin/fyp Раньше это представляло некоторые трудности, но сейчас это вполне быстро и просто, только вводите валидные реальные данные.


После регистрации в сможете создать Applicationб к которому вам выдадут AppId, DevId и CertId. Также понадобится создать токен, но чтобы его валидировать надо будет зайти под вашим аккаунтом продавца Ebay, так что на момент подключения уже нужно будет, чтобы наш клиент зарегистрировал аккаунт.


5.3. Сохраняем авторизационные данные для АПИ


Эти данные мы сохраним в string константы, независимо от планируемого кол-ва аккаунтов Ebay, так как набор категорий всего один для любого аккаунта и любого юзера.


Итого, нам нужны константы типа string


EbayAppId
EbayAppToken
EbayDevId
EbayCertId

Также сразу внесем в константы адрес вебсервиса Ebay, локаль и версию апи, которую мы используем.


EbayEndPoint
EbayVersion
EbaySiteId – 215, если хотите вкачать категории на русском.

5.4 Импортируем wsdl


Сама wsdl расположена по адресу http://developer.ebay.com/webservices/latest/ebaySvc.wsdl
На случай, если адрес сменится, его можно найти в https://go.developer.ebay.com/api-documentation.
Нам надо будет сгенерировать класс или wsdl.
Напоминаем, как это делается.


Берём утилиту wsdl.exe (лежит примерно тут c:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools), которая в текущем каталоге создаёт полученные интеграционные классы.


Пример запроса:


>"c:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\wsdl.exe http://wstest.dpd.ru/services/geography?wsdl /out:MyEbayclass.cs

Замечания:


a) не используйте x64 версию. у меня отказалась генерировать


б) используйте ту версию фрейморка, под которым планиру

© Habrahabr.ru