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}