rust_yandexmarket/
lib.rs

1// lib.rs
2//! # rust-yandexmarket
3//!
4//! Библиотека для работы с API Yandex.Market на языке программирования Rust
5
6use anyhow::Result;
7use reqwest::Url;
8use secrecy::{ExposeSecret, Secret};
9use tracing::{error, instrument};
10
11use crate::models::{AddOffersToArchiveErrorDto, AddOffersToArchiveRequest, AddOffersToArchiveResponse, ApiErrorResponse, ApiResponseStatusType, CalculateTariffsOfferDto, CalculateTariffsParametersDto, CalculateTariffsRequest, CalculateTariffsResponse, CampaignDto, CategoryDto, ConfirmPricesRequest, DeleteOffersFromArchiveRequest, DeleteOffersFromArchiveResponse, DeleteOffersRequest, DeleteOffersResponse, GetCampaignOfferDto, GetCampaignOffersRequest, GetCampaignOffersResponse, GetCampaignsResponse, GetCategoriesMaxSaleQuantumRequest, GetCategoriesMaxSaleQuantumResponse, GetCategoriesResponse, GetCategoryContentParametersResponse, GetOfferMappingDto, GetOfferMappingsRequest, GetOfferMappingsResponse, GetOfferRecommendationsRequest, GetOfferRecommendationsResponse, GetQuarantineOffersRequest, GetQuarantineOffersResponse, OfferRecommendationDto, PaymentFrequencyType, QuarantineOfferDto, SellingProgramType, UpdateBusinessOfferPriceDto, UpdateBusinessPricesRequest, UpdateCampaignOfferDto, UpdateCampaignOffersRequest, UpdateOfferMappingDto, UpdateOfferMappingResultDto, UpdateOfferMappingsRequest, UpdateOfferMappingsResponse, UpdateStockDto, UpdateStocksRequest};
12
13pub mod models;
14
15/// Клиент для доступа к Yandex Market API.
16///
17/// # Пример
18///
19/// ```rust
20///use anyhow::Result;
21///use rust_yandexmarket::MarketClient;
22///
23/// #[tokio::main]
24/// async fn main() -> Result<()> {
25///     let subscriber = tracing_subscriber::fmt()
26///         .with_max_level(tracing::Level::DEBUG)
27///         .finish();
28///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
29///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
30///     let client = MarketClient::new(token).await?;
31///     // do something with the client
32///     Ok(())
33/// }
34///```
35///
36/// ## Как добавить новые товары в каталог
37///
38/// 1. Получите список категорий Маркета, выполнив запрос [`get_categories_tree`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/categories/getCategoriesTree).
39/// 2. Для каждой категории запросите список необходимых характеристик с помощью [`get_category_content_parameters`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/content/getCategoryContentParameters).
40/// 3. Передайте информацию о товарах (названия, описания, фотографии и так далее), цены, категории на Маркете и характеристики с помощью запроса [`update_offer_mappings`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/updateOfferMappings).
41/// 4. Чтобы узнать стоимость услуг Маркета для конкретных товаров, передайте их параметры в запросе [`tariffs_calculate`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/tariffs/calculateTariffs).
42/// 5. Получите у Маркета список моделей, по которым можно продавать каждый из добавленных товаров с помощью запроса [`offer_mappings`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/getOfferMappings). [Что такое модель работы и какие модели есть](https://yandex.ru/support/marketplace/introduction/models.html).
43/// 6. Задайте условия размещения товаров с помощью запроса [`offers_update`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/assortment/updateCampaignOffers). Условия размещения — это минимальный объем заказа, квант продаж и ставка НДС. Если вы работаете по модели DBS, этим же запросом задаются параметры доставки.
44/// 7. Убедитесь, что товары появились на витрине, с помощью запроса [`get_campaign_offers`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/assortment/getCampaignOffers).
45/// Подробные пояснения к статусам товаров вы найдете в [Справке Маркета для продавцов](https://yandex.ru/support/marketplace/assortment/add/statuses.html).
46///
47/// ## Как изменить цены на товары
48///
49/// 1. Чтобы узнать стоимость услуг Маркета для конкретных товаров, передайте их параметры в запросе [`tariffs_calculate`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/tariffs/calculateTariffs).
50/// 2. Передайте новые цены для всех магазинов с помощью запроса [`offer_prices_updates`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/updateBusinessPrices).
51/// 3. Убедитесь, что ни один из товаров не попал в карантин с помощью запроса [`price_quarantine`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/getBusinessQuarantineOffers).
52/// 4. Если карантин не пустой, проверьте цены на товары. Ошибочно установленные цены для всех магазинов можно исправить запросом [`offer_prices_updates`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/updateBusinessPrices).
53/// 5. После того как в карантине останутся только правильные цены, подтвердите их запросом [`price_quarantine_confirm`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/confirmBusinessPrices). Если ложные срабатывания карантина случаются часто, подумайте о том, чтобы изменить его порог по [инструкции](https://yandex.ru/support/marketplace/assortment/operations/prices.html#quarantine).
54///
55/// ## Как управлять товарами в архиве
56/// 1. Для архивации товаров используйте запрос [`offer_mappings_archive`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/addOffersToArchive). Если товары не удалось архивировать, они вернутся в ответе запроса.
57/// 2. Для просмотра товаров в архиве используйте фильтр `archived` в запросе [`offer_mappings`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/getOfferMappings)
58/// 3. Чтобы восстановить товар из архива, используйте запрос [`offer_mappings_unarchive`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/deleteOffersFromArchive)
59/// 
60/// ## Передача остатков по API
61/// С помощью запроса [`update_stock`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/stocks/updateStocks)
62#[derive(Debug)]
63pub struct MarketClient {
64    token: Secret<String>,
65    base_url: Url,
66    client: reqwest::Client,
67    business_id: i64,
68    campaigns: Vec<CampaignDto>,
69}
70impl MarketClient {
71    /// Создает новый экземпляр `MarketClient`.
72    ///
73    /// # Пример
74    ///
75    /// ```rust
76    ///use anyhow::Result;
77    ///use rust_yandexmarket::MarketClient;
78    ///
79    /// #[tokio::main]
80    /// async fn main() -> Result<()> {
81    ///     let subscriber = tracing_subscriber::fmt()
82    ///         .with_max_level(tracing::Level::DEBUG)
83    ///         .finish();
84    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
85    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
86    ///     let client = MarketClient::new(token).await?;
87    ///     // do something with the client
88    ///     Ok(())
89    /// }
90    ///```
91    #[instrument(skip_all)]
92    pub async fn new<T: ToString>(token: T) -> Result<Self> {
93        let token = Secret::new(token.to_string());
94        let base_url = Url::parse("https://api.partner.market.yandex.ru")?;
95        let client = reqwest::Client::builder().gzip(true).build()?;
96        let mut result = Self {
97            token,
98            base_url,
99            client,
100            business_id: 0,
101            campaigns: vec![],
102        };
103        let campaigns = result.get_campaigns().await?;
104        let business_id = campaigns
105            .first()
106            .and_then(|c| c.clone().business.and_then(|b| b.id))
107            .unwrap_or_default();
108        result.business_id = business_id;
109        result.campaigns = campaigns;
110        Ok(result)
111    }
112    #[instrument(skip(self))]
113    fn token(&self) -> String {
114        self.token.expose_secret().to_string()
115    }
116    pub fn campaign_ids(&self) -> Vec<i64> {
117        self.campaigns.iter().flat_map(|c| c.id).collect()
118    }
119    /// Возвращает дерево категорий Маркета.
120    ///
121    /// # Пример
122    ///
123    /// ```rust
124    /// use anyhow::Result;
125    /// use rust_yandexmarket::MarketClient;
126    /// use tracing::info;
127    ///
128    /// #[tokio::main]
129    /// async fn main() -> Result<()> {
130    ///     let subscriber = tracing_subscriber::fmt()
131    ///         .with_max_level(tracing::Level::DEBUG)
132    ///         .finish();
133    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
134    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
135    ///     let client = MarketClient::new(token).await?;
136    ///     let categories_tree = client.get_categories_tree().await?;
137    ///     info!("Categories tree: {:#?}", categories_tree);
138    ///     Ok(())
139    /// }
140    /// ```
141    #[instrument(skip(self))]
142    pub async fn get_categories_tree(&self) -> Result<GetCategoriesResponse> {
143        let uri = self.base_url.join("categories/tree")?;
144        let result: GetCategoriesResponse = self
145            .client
146            .post(uri)
147            .bearer_auth(&self.token())
148            .send()
149            .await?
150            .json()
151            .await?;
152        Ok(result)
153    }
154    /// Возвращает список характеристик с допустимыми значениями для заданной категории.
155    ///
156    /// # Пример
157    ///
158    /// ```rust
159    /// use anyhow::Result;
160    /// use rust_yandexmarket::MarketClient;
161    /// use tracing::info;
162    ///
163    /// #[tokio::main]
164    /// async fn main() -> Result<()> {
165    ///     let subscriber = tracing_subscriber::fmt()
166    ///         .with_max_level(tracing::Level::DEBUG)
167    ///         .finish();
168    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
169    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
170    ///     let client = MarketClient::new(token).await?;
171    ///     info!("Client initialized successfully\n{client:#?}");
172    ///     let category_id = 91636;
173    ///     let category_content_parameters = client.get_category_content_parameters(category_id).await?;
174    ///     info!("Category content parameters: {:#?}", category_content_parameters);
175    ///     Ok(())
176    /// }
177    /// ```
178    #[instrument(skip(self))]
179    pub async fn get_category_content_parameters(
180        &self,
181        category_id: i64,
182    ) -> Result<GetCategoryContentParametersResponse> {
183        let endpoint = format!("category/{category_id}/parameters");
184        let uri = self.base_url.join(&endpoint)?;
185        let result: GetCategoryContentParametersResponse = self
186            .client
187            .post(uri)
188            .bearer_auth(&self.token())
189            .send()
190            .await?
191            .json()
192            .await?;
193        Ok(result)
194    }
195    /// Возвращает лимит на установку кванта и минимального количества товаров в заказе, которые вы можете задать для товаров указанных категорий.
196    ///
197    /// Если вы передадите значение кванта или минимального количества товаров выше установленного Маркетом ограничения, товар будет скрыт с витрины.
198    ///
199    /// # Пример
200    ///
201    /// ```rust
202    ///
203    /// use anyhow::Result;
204    /// use rust_yandexmarket::MarketClient;
205    /// use tracing::info;
206    ///
207    /// #[tokio::main]
208    /// async fn main() -> Result<()> {
209    ///     let subscriber = tracing_subscriber::fmt()
210    ///         .with_max_level(tracing::Level::DEBUG)
211    ///         .finish();
212    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
213    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
214    ///     let client = MarketClient::new(token).await?;
215    ///     let category_id = 91636;
216    ///     let category_max_sale_quantum = client.get_categories_max_sale_quantum(vec![category_id]).await?;
217    ///     info!("Category max sale quantum:\n{category_max_sale_quantum:#?}");
218    ///     Ok(())
219    /// }
220    /// ```
221    #[instrument(skip(self))]
222    pub async fn get_categories_max_sale_quantum(
223        &self,
224        categories: Vec<i64>,
225    ) -> Result<GetCategoriesMaxSaleQuantumResponse> {
226        let uri = self.base_url.join("categories/max-sale-quantum")?;
227        let body = GetCategoriesMaxSaleQuantumRequest::new(categories);
228        let response: GetCategoriesMaxSaleQuantumResponse = self
229            .client
230            .post(uri)
231            .bearer_auth(&self.token())
232            .json(&body)
233            .send()
234            .await?
235            .json()
236            .await?;
237        Ok(response)
238    }
239    /// Возвращает список магазинов, к которым имеет доступ пользователь — владелец авторизационного токена, использованного в запросе. Для агентских пользователей список состоит из подагентских магазинов.
240    ///
241    /// # Пример
242    ///
243    /// ```rust
244    /// use anyhow::Result;
245    /// use rust_yandexmarket::MarketClient;
246    /// use tracing::info;
247    ///
248    /// #[tokio::main]
249    /// async fn main() -> Result<()> {
250    ///     let subscriber = tracing_subscriber::fmt()
251    ///         .with_max_level(tracing::Level::DEBUG)
252    ///         .finish();
253    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
254    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
255    ///     let client = MarketClient::new(token).await?;
256    ///     let campaigns = client.get_campaigns().await?;
257    ///     info!("Campaigns: {:#?}", campaigns);
258    ///     Ok(())
259    /// }
260    /// ```
261    #[instrument(skip(self))]
262    pub async fn get_campaigns(&self) -> Result<Vec<CampaignDto>> {
263        let uri = self.base_url.join("campaigns")?;
264        let mut page = 1;
265        let page_size = 10;
266        let mut result = Vec::new();
267        loop {
268            let response: GetCampaignsResponse = self
269                .client
270                .get(uri.clone())
271                .query(&[("page", page), ("page_size", page_size)])
272                .bearer_auth(&self.token())
273                .send()
274                .await?
275                .json()
276                .await?;
277            match response.campaigns {
278                None => {
279                    break;
280                }
281                Some(campaigns) => {
282                    result.extend(campaigns);
283                    match response.pager.and_then(|p| p.pages_count) {
284                        None => {
285                            break;
286                        }
287                        Some(count) => {
288                            if page < count {
289                                page += 1;
290                            } else {
291                                break;
292                            }
293                        }
294                    }
295                }
296            }
297        }
298        Ok(result)
299    }
300    /// Добавляет товары в каталог, передает их категории на Маркете и характеристики, необходимые для этих категории. Также редактирует информацию об уже имеющихся товарах.
301    ///
302    /// Список категорий Маркета можно получить с помощью запроса get_categories_tree, а характеристики товаров по категориям с помощью get_category_content_parameters.
303    ///
304    /// Чтобы добавить новый товар, передайте его с новым идентификатором, который раньше никогда не использовался в каталоге. Старайтесь сразу передать как можно больше информации — она потребуется Маркету для подбора подходящей карточки или создания новой. Если известно, какой карточке на Маркете соответствует товар, можно сразу указать идентификатор этой карточки (SKU на Маркете) в поле marketSKU.
305    ///
306    /// Для новых товаров обязательно укажите параметры: offerId, name, marketCategoryId или category, pictures, vendor, description.
307    ///
308    /// Чтобы отредактировать информацию о товаре, передайте новые данные, указав в offerId соответствующий ваш SKU. Поля, в которых ничего не меняется, можно не передавать.
309    ///
310    /// # Пример
311    ///
312    /// ```rust
313    /// use anyhow::Result;
314    /// use rust_yandexmarket::models::{ParameterValueDto, UpdateOfferDto, UpdateOfferMappingDto};
315    /// use rust_yandexmarket::MarketClient;
316    /// use tracing::info;
317    ///
318    /// #[tokio::main]
319    /// async fn main() -> Result<()> {
320    ///     let subscriber = tracing_subscriber::fmt()
321    ///         .with_max_level(tracing::Level::DEBUG)
322    ///         .finish();
323    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
324    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
325    ///     let client = MarketClient::new(token).await?;
326    ///     let category_id = 6119048;
327    ///     let content_parameters = client
328    ///         .get_category_content_parameters(category_id)
329    ///         .await?
330    ///         .result
331    ///         .and_then(|r| r.parameters)
332    ///         .unwrap_or_default();
333    ///     let mut parameters = Vec::new();
334    ///     for content_parameter in content_parameters.iter() {
335    ///         if let Some(name) = content_parameter.name.as_deref() {
336    ///             let parameter_id = content_parameter.id;
337    ///             let mut unit_id = None;
338    ///             let mut value_id = None;
339    ///             let value = match name {
340    ///                 "Ширина" => {
341    ///                     unit_id = content_parameter.get_unit_id("метр");
342    ///                     "0.8"
343    ///                 }
344    ///                 "Форма" => {
345    ///                     value_id = content_parameter.get_value_id("прямоугольная");
346    ///                     "прямоугольная"
347    ///                 }
348    ///                 "Цвет товара для карточки" => {
349    ///                     value_id = content_parameter.get_value_id("75");
350    ///                     "75"
351    ///                 }
352    ///                 "Количество в наборе" => "2",
353    ///                 "Длина" => "1.5",
354    ///                 "Цвет товара для фильтра" => {
355    ///                     value_id = content_parameter.get_value_id("серый");
356    ///                     "серый"
357    ///                 }
358    ///                 "Вес" => "6.72",
359    ///                 "Толщина" => {
360    ///                     unit_id = content_parameter.get_unit_id("миллиметр");
361    ///                     "15.5"
362    ///                 }
363    ///                 "Материал основы" => {
364    ///                     value_id = content_parameter.get_value_id("джут");
365    ///                     "джут"
366    ///                 }
367    ///                 "Материал верха" => {
368    ///                     value_id = content_parameter.get_value_id("полиамид");
369    ///                     "полиамид"
370    ///                 }
371    ///                 "Тип" => {
372    ///                     value_id = content_parameter.get_value_id("ковер");
373    ///                     "ковер"
374    ///                 }
375    ///                 "Тип рисунка" => {
376    ///                     value_id = content_parameter.get_value_id("однотонный");
377    ///                     "однотонный"
378    ///                 }
379    ///                 "Способ производства" => {
380    ///                     value_id = content_parameter.get_value_id("машинный");
381    ///                     "машинный"
382    ///                 }
383    ///                 "Противоскользящая основа" => "false",
384    ///                 "Безворсовый" => "false",
385    ///                 "Вес ворса на квадратный метр" => {
386    ///                     unit_id = content_parameter.get_unit_id("г/м²");
387    ///                     "2100"
388    ///                 }
389    ///                 "Высота ворса" => {
390    ///                     unit_id = content_parameter.get_unit_id("миллиметр");
391    ///                     "13"
392    ///                 }
393    ///                 "Вес на квадратный метр" => {
394    ///                     unit_id = content_parameter.get_unit_id("г/м²");
395    ///                     "2800"
396    ///                 }
397    ///                 "Страна производства" => {
398    ///                     value_id = content_parameter.get_value_id("Бельгия");
399    ///                     "Бельгия"
400    ///                 }
401    ///                 "Набор" => "true",
402    ///                 _ => continue,
403    ///             };
404    ///             let pvd = ParameterValueDto::build()
405    ///                 .parameter_id(parameter_id)
406    ///                 .unit_id(unit_id)
407    ///                 .value_id(value_id)
408    ///                 .value(value)
409    ///                 .build()?;
410    ///             parameters.push(pvd);
411    ///         }
412    ///     }
413    ///     let pictures = vec![
414    ///         "https://safira.club/wp-content/uploads/mekota_75_large.jpeg".to_string(),
415    ///         "https://safira.club/wp-content/uploads/mekota_75_office_large.jpeg".to_string(),
416    ///     ];
417    ///     let offer = UpdateOfferDto::builder()
418    ///         .offer_id("AW Carolus 75 0.8x1.5 - 2 pcs")
419    ///         .name("Ковер AW Carolus 75 0.8x1.5 м комплект 2 штуки")
420    ///         .market_category_id(category_id)
421    ///         .category("Ковры")
422    ///         .pictures(pictures)
423    ///         .vendor("AW")
424    ///         .description("Ковёр AW Carolus – это качественный и эстетичный элемент декора из Бельгии. Это однотонный ковёр из 100% полиэстера с окантованными неширокой тесьмой краями.")
425    ///         .manufacturer_countries(vec!["Бельгия".to_string()])
426    ///         .weight_dimensions(80.0, 40.0, 40.0, 6.72)
427    ///         .vendor_code("AW Carolus 75")
428    ///         .parameter_values(parameters)
429    ///         .basic_price(27490.0, Some(35350.0))
430    ///         .purchase_price(15340.0)
431    ///         .additional_expenses(2900.0)
432    ///         .cofinance_price(21990.0)
433    ///         .build()?;
434    ///     let update_offers_mapping_dto = vec![UpdateOfferMappingDto::new(offer)];
435    ///     let not_updated_offers_result = client
436    ///         .update_offer_mappings(update_offers_mapping_dto)
437    ///         .await?;
438    ///     info!("Update result:\n{:#?}", not_updated_offers_result);
439    ///     Ok(())
440    /// }
441    ///```
442    #[instrument(skip(self, offers))]
443    pub async fn update_offer_mappings(
444        &self,
445        offers: Vec<UpdateOfferMappingDto>,
446    ) -> Result<Vec<UpdateOfferMappingResultDto>> {
447        let endpoint = format!("businesses/{}/offer-mappings/update", self.business_id);
448        let uri = self.base_url.join(&endpoint)?;
449        let mut result = Vec::new();
450        for chunk in offers.chunks(500) {
451            let body = UpdateOfferMappingsRequest::new(chunk.to_vec());
452            let response: UpdateOfferMappingsResponse = self
453                .client
454                .post(uri.clone())
455                .bearer_auth(&self.token())
456                .json(&body)
457                .send()
458                .await?
459                .json()
460                .await?;
461            let res = response.results.unwrap_or_default();
462            result.extend(res);
463        }
464        Ok(result)
465    }
466    /// Рассчитывает стоимость услуг Маркета для товаров с заданными параметрами. Порядок товаров в запросе и ответе сохраняется, чтобы определить, для какого товара рассчитана стоимость услуги.
467    ///
468    /// Обратите внимание: калькулятор осуществляет примерные расчеты. Финальная стоимость для каждого заказа зависит от предоставленных услуг.
469    ///
470    /// В запросе можно указать либо параметр `campaignId`, либо `sellingProgram`. Совместное использование параметров приведет к ошибке.
471    /// ```rust
472    /// use anyhow::Result;
473    /// use rust_yandexmarket::MarketClient;
474    /// use tracing::info;
475    /// use rust_yandexmarket::models::{CalculateTariffsOfferDto, SellingProgramType};
476    ///
477    /// #[tokio::main]
478    /// async fn main() -> Result<()> {
479    ///     let subscriber = tracing_subscriber::fmt()
480    ///         .with_max_level(tracing::Level::DEBUG)
481    ///         .finish();
482    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
483    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
484    ///     let client = MarketClient::new(token).await?;
485    ///     let offers = vec![CalculateTariffsOfferDto::new(
486    ///         6119048,
487    ///         21990.0,
488    ///         0.8,
489    ///         0.4,
490    ///         0.4,
491    ///         6.72,
492    ///         Some(1),
493    ///     )];
494    ///     let tariffs = client
495    ///         .tariffs_calculate(None, Some(SellingProgramType::Dbs), None, offers)
496    ///         .await?;
497    ///     info!("Tariffs: {:#?}", tariffs);
498    ///     Ok(())
499    /// }
500    ///```
501    #[instrument(skip(self, offers))]
502    pub async fn tariffs_calculate(
503        &self,
504        campaign_id: Option<i64>,
505        selling_program: Option<SellingProgramType>,
506        frequency: Option<PaymentFrequencyType>,
507        offers: Vec<CalculateTariffsOfferDto>,
508    ) -> Result<CalculateTariffsResponse> {
509        let uri = self.base_url.join("tariffs/calculate")?;
510        let parameters =
511            CalculateTariffsParametersDto::new(campaign_id, selling_program, frequency);
512        let body = CalculateTariffsRequest::new(parameters, offers);
513        let response: CalculateTariffsResponse = self
514            .client
515            .post(uri)
516            .bearer_auth(&self.token())
517            .json(&body)
518            .send()
519            .await?
520            .json()
521            .await?;
522        Ok(response)
523    }
524    /// Возвращает список товаров в каталоге, их категории на Маркете и характеристики каждого товара.
525    /// Можно использовать тремя способами:
526    ///
527    /// Задать список интересующих SKU;
528    /// задать фильтр — в этом случае результаты возвращаются постранично;
529    /// не передавать тело запроса, чтобы получить список всех товаров в каталоге.
530    ///
531    /// # Пример
532    ///
533    /// ```rust
534    /// use anyhow::Result;
535    /// use rust_yandexmarket::MarketClient;
536    /// use tracing::info;
537    /// use rust_yandexmarket::models::GetOfferMappingsRequest;
538    ///
539    /// #[tokio::main]
540    /// async fn main() -> Result<()> {
541    ///     let subscriber = tracing_subscriber::fmt()
542    ///         .with_max_level(tracing::Level::DEBUG)
543    ///         .finish();
544    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
545    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
546    ///     let client = MarketClient::new(token).await?;
547    ///     let request = Some(GetOfferMappingsRequest::builder()
548    ///         .vendor_names(vec!["Haima".to_string()])
549    ///         .build()?);
550    ///     let offer_mappings = client.offer_mappings(request).await?;
551    ///     info!("Offer mappings: {:#?}", offer_mappings);
552    ///     Ok(())
553    /// }
554    /// ```
555    #[instrument(skip(self))]
556    pub async fn offer_mappings(
557        &self,
558        body: Option<GetOfferMappingsRequest>,
559    ) -> Result<Vec<GetOfferMappingDto>> {
560        let endpoint = format!("businesses/{}/offer-mappings", self.business_id);
561        let mut uri = self.base_url.join(&endpoint)?;
562        uri.set_query(Some("limit=200"));
563        let mut page_token = None;
564        let mut result = Vec::new();
565        loop {
566            if let Some(next_page_token) = page_token.clone() {
567                let query = format!("page_token={next_page_token}");
568                uri.set_query(Some(query.as_str()))
569            }
570            let response: GetOfferMappingsResponse = if let Some(body) = body.clone() {
571                self.client
572                    .post(uri.clone())
573                    .bearer_auth(&self.token())
574                    .json(&body)
575                    .send()
576                    .await?
577                    .json()
578                    .await?
579            } else {
580                self.client
581                    .post(uri.clone())
582                    .bearer_auth(&self.token())
583                    .send()
584                    .await?
585                    .json()
586                    .await?
587            };
588            let offer_mappings = response
589                .result
590                .clone()
591                .and_then(|r| r.offer_mappings)
592                .unwrap_or_default();
593            result.extend(offer_mappings);
594            if let Some(next_page_token) = response
595                .result
596                .and_then(|r| r.paging.and_then(|p| p.next_page_token))
597            {
598                page_token = Some(next_page_token);
599            } else {
600                break;
601            }
602        }
603        Ok(result)
604    }
605    /// Изменяет параметры размещения товаров в конкретном магазине: доступность товара, условия доставки и самовывоза, применяемую ставку НДС.
606    ///
607    /// # Пример
608    ///
609    /// ```rust
610    ///
611    /// use anyhow::Result;
612    /// use rust_yandexmarket::MarketClient;
613    /// use tracing::info;
614    /// use rust_yandexmarket::models::UpdateCampaignOfferDto;
615    ///
616    /// #[tokio::main]
617    /// async fn main() -> Result<()> {
618    ///     let subscriber = tracing_subscriber::fmt()
619    ///         .with_max_level(tracing::Level::DEBUG)
620    ///         .finish();
621    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
622    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
623    ///     let client = MarketClient::new(token).await?;
624    ///     let offer = UpdateCampaignOfferDto::builder()
625    ///         .offer_id("AW Carolus 75 0.8x1.5 - 2 pcs")
626    ///         .quantum(1, 1)
627    ///         .available(true)
628    ///         .vat(6)
629    ///         .build()?;
630    ///     for campaign_id in client.campaign_ids() {
631    ///         let result = client.offers_update(campaign_id, vec![offer.clone()]).await?;
632    ///         info!("Offers update result:\n{result:#?}");
633    ///     }
634    ///     Ok(())
635    /// }
636    /// ```
637    #[instrument(skip(self, offers))]
638    pub async fn offers_update(
639        &self,
640        campaign_id: i64,
641        offers: Vec<UpdateCampaignOfferDto>,
642    ) -> Result<ApiErrorResponse> {
643        let endpoint = format!("campaigns/{campaign_id}/offers/update");
644        let uri = self.base_url.join(&endpoint)?;
645        let body = UpdateCampaignOffersRequest::new(offers);
646        let result: ApiErrorResponse = self
647            .client
648            .post(uri)
649            .bearer_auth(&self.token())
650            .json(&body)
651            .send()
652            .await?
653            .json()
654            .await?;
655        Ok(result)
656    }
657    /// Возвращает список товаров, размещенных в заданном магазине. Для каждого товара указываются параметры размещения.
658    ///
659    /// # Пример
660    ///
661    /// ```rust
662    /// use anyhow::Result;
663    /// use rust_yandexmarket::MarketClient;
664    /// use tracing::info;
665    ///
666    /// #[tokio::main]
667    /// async fn main() -> Result<()> {
668    ///     let subscriber = tracing_subscriber::fmt()
669    ///         .with_max_level(tracing::Level::DEBUG)
670    ///         .finish();
671    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
672    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
673    ///     let client = MarketClient::new(token).await?;
674    ///     for campaign_id in client.campaign_ids() {
675    ///         let offers = client.get_campaign_offers(campaign_id, None).await?;
676    ///         info!("Offers: {:#?}", offers);
677    ///     }
678    ///     Ok(())
679    /// }
680    ///```
681    #[instrument(skip(self))]
682    pub async fn get_campaign_offers(
683        &self,
684        campaign_id: i64,
685        get_campaign_offers_request: Option<GetCampaignOffersRequest>,
686    ) -> Result<Vec<GetCampaignOfferDto>> {
687        let endpoint = format!("campaigns/{}/offers", campaign_id);
688        let mut uri = self.base_url.join(&endpoint)?;
689        uri.set_query(Some("limit=200"));
690        let mut page_token = None;
691        let mut result = Vec::new();
692        loop {
693            if let Some(next_page_token) = page_token {
694                let query = format!("page_token={}", next_page_token);
695                uri.set_query(Some(query.as_str()));
696            }
697            let body = get_campaign_offers_request.clone().unwrap_or_default();
698            let response: GetCampaignOffersResponse = self
699                .client
700                .post(uri.clone())
701                .bearer_auth(&self.token())
702                .json(&body)
703                .send()
704                .await?
705                .json()
706                .await?;
707            let offers = response
708                .result
709                .clone()
710                .and_then(|r| r.offers)
711                .unwrap_or_default();
712            result.extend(offers);
713            match response
714                .result
715                .and_then(|r| r.paging.and_then(|s| s.next_page_token))
716            {
717                None => break,
718                Some(next_page_token) => {
719                    page_token = Some(next_page_token);
720                }
721            }
722        }
723        Ok(result)
724    }
725    /// Удаляет товары из каталога.
726    ///
727    /// # Пример
728    ///
729    /// ```rust
730    /// use anyhow::Result;
731    /// use rust_yandexmarket::MarketClient;
732    /// use tracing::info;
733    /// use rust_yandexmarket::models::GetOfferMappingsRequest;
734    ///
735    /// #[tokio::main]
736    /// async fn main() -> Result<()> {
737    ///     let subscriber = tracing_subscriber::fmt()
738    ///         .with_max_level(tracing::Level::DEBUG)
739    ///         .finish();
740    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
741    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
742    ///     let client = MarketClient::new(token).await?;
743    ///     let req = GetOfferMappingsRequest::builder()
744    ///         .vendor_names(vec!["Haima".to_string()])
745    ///         .build()?;
746    ///     let offer_ids = client.offer_mappings(Some(req)).await?.into_iter().flat_map(|o| o.offer.map(|f| f.offer_id)).collect::<Vec<_>>();
747    ///     let not_deleted_offers = client.delete_offers(offer_ids).await?;
748    ///     info!("Not deleted offers: {not_deleted_offers:#?}");
749    ///     Ok(())
750    /// }
751    /// ```
752    #[instrument(skip(self, offer_ids))]
753    pub async fn delete_offers(&self, offer_ids: Vec<String>) -> Result<Vec<String>> {
754        let endpoint = format!("businesses/{}/offer-mappings/delete", self.business_id);
755        let uri = self.base_url.join(&endpoint)?;
756        let mut not_deleted_offers = Vec::new();
757        for offer_ids_chunk in offer_ids.chunks(200) {
758            let body = DeleteOffersRequest::new(offer_ids_chunk.to_vec());
759            let result: DeleteOffersResponse = self
760                .client
761                .post(uri.clone())
762                .bearer_auth(&self.token())
763                .json(&body)
764                .send()
765                .await?
766                .json()
767                .await?;
768            let not_deleted = result
769                .result
770                .clone()
771                .and_then(|r| r.not_deleted_offer_ids)
772                .unwrap_or_default();
773            not_deleted_offers.extend(not_deleted);
774            if let Some(status) = result.status {
775                match status {
776                    ApiResponseStatusType::Ok => {}
777                    ApiResponseStatusType::Error => {
778                        error!("Error deleting offers: {:#?}", result);
779                        break;
780                    }
781                }
782            }
783        }
784        Ok(not_deleted_offers)
785    }
786    /// Устанавливает базовые цены. Чтобы получить рекомендации Маркета, касающиеся цен, выполните запрос [`offers_recommendations`](https://yandex.ru/dev/market/partner-api/doc/ru/reference/business-assortment/getOfferRecommendations)
787    ///
788    /// # Пример
789    ///
790    /// ```rust
791    /// use anyhow::Result;
792    /// use rust_yandexmarket::MarketClient;
793    /// use rust_yandexmarket::models::UpdateBusinessOfferPriceDto;
794    ///
795    /// #[tokio::main]
796    /// async fn main() -> Result<()> {
797    ///     let subscriber = tracing_subscriber::fmt()
798    ///         .with_max_level(tracing::Level::DEBUG)
799    ///         .finish();
800    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
801    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
802    ///     let client = MarketClient::new(token).await?;
803    ///     let offer_id = "AW Mambo 99 50x50";
804    ///     let body = vec![UpdateBusinessOfferPriceDto::new(offer_id, 26925.0, Some(31690.0))];
805    ///     client.offer_prices_updates(body).await?;
806    ///     Ok(())
807    /// }
808    /// ```
809    #[instrument(skip(self, offers))]
810    pub async fn offer_prices_updates(
811        &self,
812        offers: Vec<UpdateBusinessOfferPriceDto>,
813    ) -> Result<()> {
814        let endpoint = format!("businesses/{}/offer-prices/updates", self.business_id);
815        let uri = self.base_url.join(&endpoint)?;
816        for chunk in offers.chunks(500) {
817            let body = UpdateBusinessPricesRequest::new(chunk.to_vec());
818            let result: ApiErrorResponse = self
819                .client
820                .post(uri.clone())
821                .bearer_auth(&self.token())
822                .json(&body)
823                .send()
824                .await?
825                .json()
826                .await?;
827
828            if result
829                .status
830                .is_some_and(|s| s == ApiResponseStatusType::Error)
831            {
832                error!("Error:\n{:?}", result.errors)
833            }
834        }
835        Ok(())
836    }
837    /// Рекомендации Маркета, касающиеся цен
838    ///
839    /// # Пример
840    ///
841    /// ```rust
842    /// use anyhow::Result;
843    /// use rust_yandexmarket::MarketClient;
844    /// use tracing::info;
845    ///
846    /// #[tokio::main]
847    /// async fn main() -> Result<()> {
848    ///     let subscriber = tracing_subscriber::fmt()
849    ///         .with_max_level(tracing::Level::DEBUG)
850    ///         .finish();
851    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
852    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
853    ///     let client = MarketClient::new(token).await?;
854    ///     let recommendations = client.offers_recommendations(None).await?;
855    ///     info!("{recommendations:#?}");
856    ///     Ok(())
857    /// }
858    /// ```
859    #[instrument(skip(self, offer_ids))]
860    pub async fn offers_recommendations(
861        &self,
862        offer_ids: Option<Vec<String>>,
863    ) -> Result<Vec<OfferRecommendationDto>> {
864        let endpoint = format!("businesses/{}/offers/recommendations", self.business_id);
865        let mut uri = self.base_url.join(&endpoint)?;
866        uri.set_query(Some("limit=200"));
867        let mut page_token = None;
868        let mut result = Vec::new();
869        let body = GetOfferRecommendationsRequest::new(offer_ids);
870        loop {
871            if let Some(next_page_token) = page_token {
872                let query = format!("page_token={}", next_page_token);
873                uri.set_query(Some(query.as_str()));
874            }
875            let response: GetOfferRecommendationsResponse = self
876                .client
877                .post(uri.clone())
878                .bearer_auth(&self.token())
879                .json(&body)
880                .send()
881                .await?
882                .json()
883                .await?;
884            let offers = response
885                .result
886                .clone()
887                .and_then(|r| r.offer_recommendations)
888                .unwrap_or_default();
889            result.extend(offers);
890            match response
891                .result
892                .and_then(|r| r.paging.and_then(|s| s.next_page_token))
893            {
894                None => break,
895                Some(next_page_token) => {
896                    page_token = Some(next_page_token);
897                }
898            }
899        }
900        Ok(result)
901    }
902    /// Возвращает список товаров, которые находятся на карантине по основной цене. Основная цена задается в каталоге и действует во всех магазинах кабинета.
903    ///
904    /// # Пример
905    ///
906    /// ```rust
907    /// use anyhow::Result;
908    /// use rust_yandexmarket::MarketClient;
909    /// use tracing::info;
910    ///
911    /// #[tokio::main]
912    /// async fn main() -> Result<()> {
913    ///     let subscriber = tracing_subscriber::fmt()
914    ///         .with_max_level(tracing::Level::DEBUG)
915    ///         .finish();
916    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
917    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
918    ///     let client = MarketClient::new(token).await?;
919    ///     let quarantine = client.price_quarantine(None).await?;
920    ///     info!("{quarantine:#?}");
921    ///     Ok(())
922    /// }
923    /// ```
924    #[instrument(skip(self))]
925    pub async fn price_quarantine(
926        &self,
927        req: Option<GetQuarantineOffersRequest>,
928    ) -> Result<Vec<QuarantineOfferDto>> {
929        let endpoint = format!("businesses/{}/price-quarantine", self.business_id);
930        let mut uri = self.base_url.join(&endpoint)?;
931        uri.set_query(Some("limit=200"));
932        let mut page_token = None;
933        let mut result = Vec::new();
934        let body = req.unwrap_or_default();
935        loop {
936            if let Some(next_page_token) = page_token {
937                let query = format!("page_token={}", next_page_token);
938                uri.set_query(Some(query.as_str()));
939            }
940            let response: GetQuarantineOffersResponse = self
941                .client
942                .post(uri.clone())
943                .bearer_auth(&self.token())
944                .json(&body)
945                .send()
946                .await?
947                .json()
948                .await?;
949            let offers = response
950                .result
951                .clone()
952                .and_then(|r| r.offers)
953                .unwrap_or_default();
954            result.extend(offers);
955            match response
956                .result
957                .and_then(|r| r.paging.and_then(|s| s.next_page_token))
958            {
959                None => break,
960                Some(next_page_token) => {
961                    page_token = Some(next_page_token);
962                }
963            }
964        }
965        Ok(result)
966    }
967    /// Подтверждает основную цену на товары, которые попали в карантин.
968    ///
969    /// # Пример
970    ///
971    /// ```rust
972    ///
973    /// use anyhow::Result;
974    /// use rust_yandexmarket::MarketClient;
975    /// use tracing::info;
976    ///
977    /// #[tokio::main]
978    /// async fn main() -> Result<()> {
979    ///     let subscriber = tracing_subscriber::fmt()
980    ///         .with_max_level(tracing::Level::DEBUG)
981    ///         .finish();
982    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
983    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
984    ///     let client = MarketClient::new(token).await?;
985    ///     let quarantine = client.price_quarantine(None).await?;
986    ///     info!("{quarantine:#?}");
987    ///     let offer_ids = quarantine
988    ///         .into_iter()
989    ///         .flat_map(|q| q.offer_id)
990    ///         .collect::<Vec<_>>();
991    ///     client.price_quarantine_confirm(offer_ids).await?;
992    ///     Ok(())
993    /// }
994    /// ```
995    #[instrument(skip(self, offer_ids))]
996    pub async fn price_quarantine_confirm(&self, offer_ids: Vec<String>) -> Result<()> {
997        let endpoint = format!("businesses/{}/price-quarantine/confirm", self.business_id);
998        let uri = self.base_url.join(&endpoint)?;
999        for chunk in offer_ids.chunks(200) {
1000            let body = ConfirmPricesRequest::new(chunk.to_vec());
1001            let response: ApiErrorResponse = self
1002                .client
1003                .post(uri.clone())
1004                .bearer_auth(self.token())
1005                .json(&body)
1006                .send()
1007                .await?
1008                .json()
1009                .await?;
1010            if response
1011                .status
1012                .is_some_and(|s| s == ApiResponseStatusType::Error)
1013            {
1014                error!("Error:\n{:?}", response.errors)
1015            }
1016        }
1017        Ok(())
1018    }
1019    /// Помещает товары в архив. Товары, помещенные в архив, скрыты с витрины во всех магазинах кабинета.
1020    ///
1021    /// # Пример
1022    ///
1023    /// ```rust
1024    /// use anyhow::Result;
1025    /// use rust_yandexmarket::MarketClient;
1026    /// use tracing::info;
1027    ///
1028    /// #[tokio::main]
1029    /// async fn main() -> Result<()> {
1030    ///     let subscriber = tracing_subscriber::fmt()
1031    ///         .with_max_level(tracing::Level::DEBUG)
1032    ///         .finish();
1033    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
1034    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
1035    ///     let client = MarketClient::new(token).await?;
1036    ///     let offer_ids = vec![String::from("AW Carolus 75 0.8x1.5 - 2 pcs")];
1037    ///     let not_archived = client
1038    ///         .offer_mappings_archive(offer_ids.clone())
1039    ///         .await?;
1040    ///     info!("Not archived: {:#?}", not_archived);
1041    ///     let not_unarchived = client
1042    ///         .offer_mappings_unarchive(offer_ids)
1043    ///         .await?;
1044    ///     info!("Not unarchived: {:#?}", not_unarchived);
1045    ///     Ok(())
1046    /// }
1047    /// ```
1048    #[instrument(skip(self, offer_ids))]
1049    pub async fn offer_mappings_archive(
1050        &self,
1051        offer_ids: Vec<String>,
1052    ) -> Result<Vec<AddOffersToArchiveErrorDto>> {
1053        let endpoint = format!("businesses/{}/offer-mappings/archive", self.business_id);
1054        let uri = self.base_url.join(&endpoint)?;
1055        let mut result = Vec::new();
1056        for chunk in offer_ids.chunks(200) {
1057            let body = AddOffersToArchiveRequest::new(chunk.to_vec());
1058            let response: AddOffersToArchiveResponse = self
1059                .client
1060                .post(uri.clone())
1061                .bearer_auth(self.token())
1062                .json(&body)
1063                .send()
1064                .await?
1065                .json()
1066                .await?;
1067            let not_archived_offers = response
1068                .result
1069                .and_then(|r| r.not_archived_offers)
1070                .unwrap_or_default();
1071            result.extend(not_archived_offers)
1072        }
1073        Ok(result)
1074    }
1075    /// Восстанавливает товары из архива.
1076    ///
1077    /// # Пример
1078    ///
1079    /// ```rust
1080    /// use anyhow::Result;
1081    /// use rust_yandexmarket::MarketClient;
1082    /// use tracing::info;
1083    ///
1084    /// #[tokio::main]
1085    /// async fn main() -> Result<()> {
1086    ///     let subscriber = tracing_subscriber::fmt()
1087    ///         .with_max_level(tracing::Level::DEBUG)
1088    ///         .finish();
1089    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
1090    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
1091    ///     let client = MarketClient::new(token).await?;
1092    ///     let offer_ids = vec![String::from("AW Carolus 75 0.8x1.5 - 2 pcs")];
1093    ///     let not_archived = client
1094    ///         .offer_mappings_archive(offer_ids.clone())
1095    ///         .await?;
1096    ///     info!("Not archived: {:#?}", not_archived);
1097    ///     let not_unarchived = client
1098    ///         .offer_mappings_unarchive(offer_ids)
1099    ///         .await?;
1100    ///     info!("Not unarchived: {:#?}", not_unarchived);
1101    ///     Ok(())
1102    /// }
1103    /// ```
1104    #[instrument(skip(self, offer_ids))]
1105    pub async fn offer_mappings_unarchive(&self, offer_ids: Vec<String>) -> Result<Vec<String>> {
1106        let endpoint = format!("businesses/{}/offer-mappings/unarchive", self.business_id);
1107        let uri = self.base_url.join(&endpoint)?;
1108        let mut result = Vec::new();
1109        for chunk in offer_ids.chunks(200) {
1110            let body = DeleteOffersFromArchiveRequest::new(chunk.to_vec());
1111            let response: DeleteOffersFromArchiveResponse = self
1112                .client
1113                .post(uri.clone())
1114                .bearer_auth(self.token())
1115                .json(&body)
1116                .send()
1117                .await?
1118                .json()
1119                .await?;
1120            let not_archived_offers = response
1121                .result
1122                .and_then(|r| r.not_unarchived_offer_ids)
1123                .unwrap_or_default();
1124            result.extend(not_archived_offers)
1125        }
1126        Ok(result)
1127    }
1128    /// Передает данные об остатках товаров на витрине.
1129    ///
1130    /// Обязательно указывайте SKU в точности так, как он указан в каталоге. Например, 557722 и 0557722 — это два разных SKU.
1131    ///
1132    /// # Пример
1133    ///
1134    /// ```rust
1135    /// use anyhow::Result;
1136    /// use rust_yandexmarket::MarketClient;
1137    /// use rust_yandexmarket::models::UpdateStockDto;
1138    /// 
1139    /// #[tokio::main]
1140    /// async fn main() -> Result<()> {
1141    ///     let subscriber = tracing_subscriber::fmt()
1142    ///         .with_max_level(tracing::Level::DEBUG)
1143    ///         .finish();
1144    ///     tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
1145    ///     let token = std::env::var("MARKET_TOKEN").expect("MARKET_TOKEN must be set");
1146    ///     let client = MarketClient::new(token).await?;
1147    ///     let stock_item = UpdateStockDto::new("AW Carolus 75 0.8x1.5 - 2 pcs", vec![4.6]);
1148    ///     dbg!(&stock_item);
1149    ///     for campaign_id in client.campaign_ids() {
1150    ///         client.update_stock(campaign_id, vec![stock_item.clone()]).await?;
1151    ///     }
1152    ///     Ok(())
1153    /// }
1154    /// ```
1155    #[instrument(skip(self, stock))]
1156    pub async fn update_stock(&self, campaign_id: i64, stock: Vec<UpdateStockDto>) -> Result<()> {
1157        let endpoint = format!("campaigns/{campaign_id}/offers/stocks");
1158        let uri = self.base_url.join(&endpoint)?;
1159        let body = UpdateStocksRequest::new(stock);
1160        let result: ApiErrorResponse = self
1161            .client
1162            .put(uri)
1163            .bearer_auth(self.token())
1164            .json(&body)
1165            .send()
1166            .await?
1167            .json()
1168            .await?;
1169        if result
1170            .status
1171            .is_some_and(|s| s == ApiResponseStatusType::Error)
1172        {
1173            error!("Error:\n{:?}", result.errors)
1174        }
1175        Ok(())
1176    }
1177}
1178pub trait SearchByName {
1179    fn search_by_name(&self, search_string: &str) -> Option<CategoryDto>;
1180}
1181impl SearchByName for Vec<CategoryDto> {
1182    fn search_by_name(&self, search_string: &str) -> Option<CategoryDto> {
1183        for category in self {
1184            if category
1185                .name
1186                .to_lowercase()
1187                .contains(&search_string.to_lowercase())
1188            {
1189                return Some(category.clone());
1190            } else if let Some(children) = category.children.clone() {
1191                if let Some(category) = children.search_by_name(search_string) {
1192                    return Some(category);
1193                }
1194            }
1195        }
1196        None
1197    }
1198}