Разработчик партнера согласился поделится личным опытом интеграции с EBay.
Мы постараемся пошагово разобрать весь процесс, укажем, на какие детали подключения стоит обратить особо тщательное внимание; какую базовую логику понадобится реализовать в обработчиках и сервисах вашей конфигурации для регулярного взаимодействия с EBAY. Ну а также, осветим типичные трудности и неудобства, с которыми совершенно неизбежно столкнетесь при данной интеграции (и вы, и заказчики).
1.Особенности Ebay
В первую очередь, конечно, неприятные особенности работы Ebay, с которыми придется иметь дело не только разработчику:
Ebay не любит РФ. Техническая поддержка – урезанная, некоторые функции в самом родном кабинете продавца Ebay могут периодически отваливаться. Подключают API неохотно.
Конечно, не проблема торговать в качестве нероссийского продавца. И указать в настройках другую country of origin. Но понадобится реальное юрлицо (в моем случае специально регистрировали китайское, например), телефон с кодом указанной страны для подтверждения. PayPal, как основная система оплаты очень тщательно проверяет данные, особенно на юрлица и продавцов. Billing address и прочие данные должны быть реальным
- 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 Еще немного настроек:
- зайти в Международные настройки,
- заполнить адрес и прочее
- Создать Правила
Речь идет о правилах Оплаты, Доставки, Возврата.
При клике на них клиент отправляется в соответствующий пункт кабинета, где создает политики для каждого из этих разделов.
Важно!
Кабинет дает создать сколько угодно политик. НО MIP принимает только 1 политику оплаты, 1 доставки, 1 возврата. Поэтому задача в настройках в рамках одной записи задать все возможные правила: Для Любого веса, Региона и так далее.
- Политике предлагается дать имя, ограничений на ввод нет. Но для 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, то увидим одинаковую структуру
Фиды распределены по папкам. В папках лежат одноименные фиды (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
Описательная инфа по товарам. Название, атрибуты(характеристики), категория.
Все обязательные теги смотрим в скачанном примере.
<productRequest>
<product>
<SKU>369866</SKU>
<productInformation localizedFor="en_US">
<title>LCD cable for Acer for Aspire 3820, 3820T, 3820G, 3820TG, 3820TZ</title>
<description>
<productDescription>LCD cable for Acer for Aspire 3820, 3820T, 3820G, 3820TG, 3820TZ</productDescription>
</description>
<pictureURL>http://oursite.com/good_big_pics/369866.jpg</pictureURL>
<category categoryType="EBAY_LEAF_CATEGORY">168061</category> <!-- categoryType should be EBAY_LEAF_CATEGORY for the category to be applied. -->
<conditionInfo>
<condition>New</condition>
<conditionDescription>Brand new</conditionDescription>
</conditionInfo>
</productInformation>
</product>
</productRequest>
Замечания:
-<condition>New</condition>
<conditionDescription>Brand new</conditionDescription>
Можно считать константой. Разобраться лично нам с ограничениями по постингу б/у товаров не удалось — Названия состояний Enumerable, которые фиг раскопаешь в документации. Товар должен быть new.
Все теги с атрибутом localizedFor="en_US" подразумевают, что для одного товара можно разместить разную информацию для разных подсайтов ebay, соответствующих указанной локали (например, разные названия для En и Ru)
4.1.2 Availability
Передает только наличие в штуках.
<inventoryRequest>
<inventory>
<SKU>369866</SKU>
<totalShipToHomeQuantity>5</totalShipToHomeQuantity>
</inventory>
</inventoryRequest>
4.1.3 Distribution
Цены, лимиты покупки, ПОЛИТИКИ, о которых мы говорили в 3.2
Тут как раз понадобятся англ. Названия Доставки, Возврата и Оплаты.
<listingDetails>
<shippingPolicyName>BaseShipping</shippingPolicyName> <!-- Replace business policy names with your seller's business policy names. -->
<maxQuantityPerBuyer>5</maxQuantityPerBuyer>
<paymentPolicyName>Base</paymentPolicyName> <!-- Replace business policy names with your seller's business policy names. -->
<returnPolicyName>BaseReturn</returnPolicyName> <!-- Replace business policy names with your seller's business policy names. -->
<pricingDetails>
<listPrice>7.58</listPrice>
</pricingDetails>
Валюта тут не указывается, она указывается клиентом в личном кабинете.
4.1.4 Взаимосвязь фидов
Правила постинга фидов очень просты
Вы сами задаете периодичность, с которой они постятся в Ebay, лимитов нет. Соответственно таск, который будет генерировать фиды, настраиваете сообразно графику работы других тасков, обновляющих существенные параметры товаров (наличие, цены). Слишком часто его дергать не следует, благо у MIP своя очередь, и порой объемный фид может стоять в очереди по пару часов.
Уникальным идентификатором товара в 3х фидах служит тег SKU, соответственно лучше не оригинальничать, и не добавлять туда сложные конструкции с партномерами, а постить что-то столь же однозначное и уникальное – наш артикул товара.
Фиды «воспринимаются» по очереди. Сначала должен уйти на фтп Product, потом Availabiliy, потом Distribution.
Снятие товаров производится отсылкой наличия = 0.
Поэтому заблаговременно предусмотрите таблицу, в которой будете хранить товары, которые уже запостили в MIP
Каждый xml пакуется в zip и кладется на фтп, в одноименную папку. Предусмотрите библиотеку для работы с фтп.
Мы брали Renci.SshNet.SftpClient
Пример метода загрузки файла этой библиотекой
private void Upload(long accountId, string zipFileName, string innerPath) { var EbayAccount = DictionaryManager.GetRecord<EbayAccountSettings>(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); } } }
- спустя время (зависящее от очереди ebay) результат обработки MIP кладет на фтп в ту же папку, в подпапку
Output/{current_date:M-dd-YYYY}/…. .xml
Например:
Мы загружаем наш файл с остатками на FTP
В папку /store/availability
Результат выплюнется в /store/availability/output/Jul-18-2016/
Также можно параллельно грузить или просто смотреть результаты обработки фидов в кабинете в Архиве лент
- В 99% случаев, если и была какая-то ошибка, на этапе Product.xml и Availability.xml MIP промолчит и скажет, что все ок. Ошибка будет, разве что у вас откровенно malformed xml.
Даже если ошибки связаны с этими фидами, ошибки будут видны только в результате обработки последнего фида, Distribution.xml
- Вопрос о том, имеет ли смысл парсить результаты выгрузки к нам в ерп – ну крайне спорный.
Никакой системности, сериализуемости в ошибках нет.
Более того, по процентам 70 из них приходится обращаться письменно в поддержку MIP, что само по себе мероприятие с 50% результативностью. Запросы часто приходится высылать несколько раз, так как успешность обращения зависит от сотрудника, на которого попадешь.
Гугл ошибок помогает мало.
Также, чрезвычайно часто выясняется, что просто у самого Mip был какой-то issue, о тех. подробностях которого они не распространяются, просто сообщая, что Пользуйтесь на здоровье, мы пофиксили.
Также, наличие в ответе ошибки не гарантирует, что все остальные товары не прогрузились.
Иногда одна ошибка блокирует все товары, даже те, к которым она не относится, иногда только из всего фида.
Суммируя, можно сказать, что имеет смысл просто следить за результатами выгрузки в кабинете.
Ради тавтологии можно остановиться на таком решении:
Парсить по датам содержимое папки /output/distribution. В Xml захватывать только теги
И в текстово-информативно-списочном варианте просто дублировать их в отдельный справочник в ерп с датами, есл клиент хочет смотреть ошибки в ерп, а не в кабинете.
Пример дефектного ответа
<?xml version='1.0' encoding='UTF-8'?>
<!-- RequestID: 5089523166 --><distributionResponse>
<responseMessage>
<SKU>6124</SKU>
<channelID>EBAY_RU</channelID>
<status>FAILURE</status>
<error>
<errorID>API_95</errorID>
<severity>ERROR</severity>
<category>REQUEST</category>
<message><![CDATA[Указанная валюта аукциона не соответствует валюте аукциона для выбранного сайта.]]></message>
</error>
</responseMessage>
<status>FAILURE</status>
</distributionResponse>
Итого, попробуем сгенерировать и залить последовательно 3 фида.
var startDate = DateTime.Now; //Поставим дату начала выгрузки, чтобы пометить ей товары, попавшие в текущую выборку
var accountId = 1L; //Выбирам аккаунт
var account = DictionaryManager.GetRecord<EbayAccountSetting>(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<string, object>{ {"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<string, object>{ {"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<string, object>{ {"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).
Фид заказа содержит следующую важную информацию
Инфа о клиенте: Фио, имейл, телефон
Адрес, подробно, включая zip
Список товаров с уникальным идентификатором, который вы передавали в фидах 1-3 в теге SKU
Цену товаров
Не забывайте, что никак нельзя получать в момент импорта актуальную цену по колонке, потому что неизвестно какой была цена того фида, по которому клиент увидел и купил этот товар. Ее берем из Ebay.
- Цену доставки
Итого алгоритм импорта должен выглядеть грубо так:
- Берем номер заказа Ebay. Проверяем по нему не импортировали ли уже его.
- Делаем GetOrCreateAgent – по данным (#Фио, имейл, телефон#)
- Делаем у агента GetOrCreateDeliveryAddress
- Создаем резерв, заполняем тч товаров с нужными ценами.
- Активируем тч доставки, проставляем код адреса и цену из Ebay.
Пример фида
<?xml version='1.0' encoding='UTF-8'?>
<pendingOrderFulfillmentResponse>
<pendingOrderFulfillment>
<order>
<orderID>222034713176-1766499808012</orderID>
<channelID>EBAY_US</channelID>
<createdDate>2016-07-20T12:45:09.000Z</createdDate>
<buyer>
<buyerID>andreyka.mosin.1997-3</buyerID>
<firstName>Андрей</firstName>
<lastName>Мосин</lastName>
<email>andreyka.mosin.1997@mail.ru</email>
</buyer>
<seller>
<sellerID>pdirect.ru</sellerID>
</seller>
<logisticsPlan fulfillmentType="SHIP">
<shipping shippingType="DIRECT">
<shippingService>
<service/>
<method>RU_ExpeditedMoscowOnly</method>
</shippingService>
<shipToAddress>
<addressID>4504234220015</addressID>
<name>Андрей Мосин</name>
<phone>9527720655</phone>
<addressLine1>курчатова 16</addressLine1>
<addressLine2/>
<city>саров</city>
<stateOrProvince/>
<postalCode>607183</postalCode>
<country>RU</country>
</shipToAddress>
<expectedDeliveryDate/>
</shipping>
</logisticsPlan>
<lineItem>
<lineItemID>222034713176-1766499808012</lineItemID>
<listing>
<channelID>CustomCode</channelID>
<itemID>222034713176</itemID>
<SKU>413480</SKU>
<title>ELM327 BlueTooth V1.5 BIG автосканер ELM327 BlueTooth V1.5 BIG chip pic18f25k80</title>
</listing>
<quantity>1</quantity>
<unitPrice currencyCode="RUB">990.0</unitPrice>
<subtotal>
<priceline type="ITEM">
<amount currencyCode="RUB">990.00</amount>
</priceline>
<priceline type="SHIPPING">
<amount currencyCode="">350.00</amount>
</priceline>
<priceline type="ITEM_TAX">
<description>SalesTax</description>
<amount currencyCode="RUB">0.00</amount>
</priceline>
<priceline type="RECYCLING">
<description>ElectronicWasteRecyclingFee</description>
<amount currencyCode="RUB">0.00</amount>
</priceline>
<sumtotal currencyCode="RUB">1340.00</sumtotal>
</subtotal>
</lineItem>
<payment>
<paymentID>3TD678480B805680J</paymentID>
<total currencyCode="RUB">1340.0</total>
</payment>
<total>
<priceline type="ITEM">
<amount currencyCode="RUB">990.0</amount>
</priceline>
<priceline type="ITEM_TAX">
<amount currencyCode="RUB">0.0</amount>
</priceline>
<priceline type="SHIPPING">
<amount currencyCode="RUB">350.0</amount>
</priceline>
<priceline type="DISCOUNT">
<amount currencyCode="RUB">0.0</amount>
</priceline>
<sumtotal currencyCode="RUB">1340.0</sumtotal>
</total>
<status>
<paymentStatus>PAID</paymentStatus>
<logisticsStatus>NOT_SHIPPED</logisticsStatus>
</status>
<note/>
</order>
</pendingOrderFulfillment>
</pendingOrderFulfillmentResponse>
Примеры импорта
var accountId = 1L; //Выбирам аккаунт
var EbayAccount = DictionaryManager.GetRecord<EbayAccountSetting>(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<EbayAccountSetting>(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<SaleDocument>(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<SaleDocument>().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<Agent>().Where(x => x.Customer.EbayUserID == ebayBuyerUserID)
select new {
Id = d.ID,
CustomerID = d.CustomerID,
PrivatePersonID = d.PrivatePersonID
}).ToList();
if (exAgents.Any())
{
customer = DictionaryManager.GetRecord<CustomerAttribute>(exAgents.First().CustomerID.GetValueOrDefault());
privatePerson = DictionaryManager.GetRecord<PrivatePersonAttribute>(exAgents.First().PrivatePersonID.GetValueOrDefault());
agent = DictionaryManager.GetRecord<Agent>(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<DeliveryAddress>(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 версию. у меня отказалась генерировать
б) используйте ту версию фрейморка, под которым планируется использование классов
Полученный класс вставим в конец обработчика, который будет получать категории
5.5.Пишем команду-импортер
Вставляем вниз кода обработчика код сгенерированного класс wsdl
Надо создать объект нашего класса чтобы позже им пользоваться.
private static object _lock = new Object();
private eBayAPIInterfaceService EbayService
{
get
{
if (_instance == null)
{
lock ( _lock)
{
if (_instance == null)
{
_instance = new eBayAPIInterfaceService();
_instance.Timeout = 1200000;
return _instance;
}
}
}
return _instance;
}
}
Получим список категорий от Ebay.
var a = Constants.Url;
var endpoint = Constants.Ebay.ApiEndpoint;
var callName = "GetCategories";
var siteId = "215" ;//Зона россия
var appId = Constants.EbayAppId;// '// use your app ID
var devId = Constants.EbayDevId;// use your dev ID
var certId = Constants.EbayCertId; // use your cert ID
var version= "405";
var token = Constants.EbayToken;
// Build the request URL
var requestURL = endpoint + "?callname=" + callName + "&siteid=" + siteId + "&appid=" + appId + "&version=" + version + "&routing=default";
EbayService.Url = requestURL;
// Set credentials
EbayService.RequesterCredentials = new CustomSecurityHeaderType();
EbayService.RequesterCredentials.eBayAuthToken = token;
EbayService.RequesterCredentials.Credentials = new UserIdPasswordType()
EbayService.RequesterCredentials.Credentials.AppId = appId;
EbayService.RequesterCredentials.Credentials.DevId = devId;
EbayService.RequesterCredentials.Credentials.AuthCert = certId;
GetCategoriesRequestType request = new GetCategoriesRequestType();
request.Version = "405";
request.CategorySiteID = "215";
request.CategoryParent = new string[] {"0"};
request.ViewAllNodes = true;
request.LevelLimit = 0;
request.DetailLevel = new DetailLevelCodeType[] {DetailLevelCodeType.ReturnAll};
var a = ConstantManager["Url"].ToString();
var endpoint = ConstantManager["Ebay. Api Endpoint"].ToString();
var callName = "GetCategories";
var siteId = "215" ;//Зона россия
var appId = ConstantManager["EbayAppId"].ToString() ;// '// use your app ID
var devId = ConstantManager["EbayDevId"].ToString();// use your dev ID
var certId = ConstantManager["EbayCertId"].ToString(); // use your cert ID
var version= "405";
var token = onstantManager["EbayToken"].ToString();
// Build the request URL
var requestURL = endpoint + "?callname=" + callName + "&siteid=" + siteId + "&appid=" + appId + "&version=" + version + "&routing=default";
GetCategoriesResponseType cats = EbayService.GetCategories(request);
if (cats.Errors != null && cats.Errors.Length > 0 )
{
foreach(var er in cats.Errors)
{
// Обработаем каждую текстовую ошибку
}
throw new Exception("Если надо выйдем из команды с ошибкой");
}
if ( cats.CategoryArray == null || cats.CategoryArray.Length <= 0)
{
// Обработаем тот факт, что пришел пустой список категорий, такого не моет быть
}
CategoryType[] catArray = cats.CategoryArray;
SyncronizeCats(-1, "", catArray); // пешем метод валидного ответа
Напишем функцию импорта строчек с категориями из Ebay, не забудем удалить у нас записи, которые не приходят больше. Лучше идти лесенкой по дереву справочника
private void SyncronizeCats(long parentID, string ebayParentCode, CategoryType[] catArray)
{
//Получим уже имеющиеся у нас категории с тем же родителем
var ourEbayCats = DictionaryManager.GetRecords<ArticleGroup>(x => (x.ParentID == parentId));
//Записываем пришедшие коды ибей
var processedCodes = new List<string>();
foreach(var ebayCat in catArray.Where(c => (string.IsNullOrEmpty(ebayParentCode) && c.CategoryLevel == 1 ) || (c.CategoryParentID != null
&& c.CategoryParentID.Any(h => h.Trim() != c.CategoryID && h.Trim() == ebayParentCode))))
{
if (!string.IsNullOrEmpty(ebayCat.CategoryID) && !string.IsNullOrEmpty(ebayCat.CategoryName))
{
GetOrCreateCategory(bayCat.CategoryID.Trim(), ebayCat.CategoryName, parentID);
processedCodes.Add(ebayCat.CategoryID.Trim());
}
}
foreach(var catRow in ourEbayCats)
{
if (!(processedCodes.Contains(catRow.EbeyCode)))
{
//Удаляем те, которых уже нет в новом списке
DictionaryManager.DeleteRecord<ArticleGroup>(catRow.ID);
}
else
{
//Вкачаем уровнем ниже
SyncronizeCats(catRow.ID, catRow.Code, catArray);
}
}
}
Ну и, наконец, апдейт или создание категории.
private void GetOrCreateCategory(string ebayCategoryID, string ebayCatCategoryName, long parentID)
{
var ebayCat = new ArticleGroup() { };
var cats = DictionaryManager.GetRecords<ArticleGroup>(x => x.Name == ebayCatCategoryName);
if (cats.Any())
{
ebayCat = cats.First();
}
else
{
ebayCat = DictionaryManager.NewRecord( new ArticleGroup() { });
}
//Проставляем парент, название и другие свойства
DictionaryManager.SaveRecord(ebayCat);
}
Итого
Технически, интеграция не представляет сложности (весь объем код не превышает нескольких киллобайт), однако в деталях всплывает немало подводных камней с некоторыми из которых мы и пытались вас познакомить.