├── CHANGELOG.md ├── LICENSE ├── README.md ├── attr_constructor.html ├── bin └── is_realized.php ├── composer.json ├── psalm.xml └── src ├── Enum ├── ClassifierStatus.php ├── DeliverySchema.php ├── DimensionUnit.php ├── Language.php ├── PostingScheme.php ├── ProductState.php ├── SortDirection.php ├── Status.php ├── TransactionType.php ├── Visibility.php └── WeightUnit.php ├── Exception ├── AccessDeniedException.php ├── BadRequestException.php ├── InternalException.php ├── NotFoundException.php ├── NotFoundInSortingCenterException.php ├── OzonSellerException.php ├── PostingNotFoundException.php ├── ProductValidatorException.php └── ValidationException.php ├── ProductValidator.php ├── Service ├── AbstractService.php ├── GetOrderInterface.php ├── HasOrdersInterface.php ├── HasUnfulfilledOrdersInterface.php ├── PassService.php ├── V1 │ ├── ActionsService.php │ ├── AnalyticsService.php │ ├── BrandService.php │ ├── CategoriesService.php │ ├── ChatService.php │ ├── FinanceService.php │ ├── Posting │ │ └── FbsService.php │ ├── ProductService.php │ ├── ReportService.php │ ├── ReturnService.php │ └── WarehouseService.php ├── V2 │ ├── AnalyticsService.php │ ├── CategoryService.php │ ├── Posting │ │ ├── CrossborderService.php │ │ ├── FboService.php │ │ └── FbsService.php │ ├── ProductService.php │ └── ReturnsService.php ├── V3 │ ├── CategoryService.php │ ├── Posting │ │ └── FbsService.php │ ├── ProductService.php │ └── ReturnService.php ├── V4 │ ├── Posting │ │ └── FbsService.php │ └── ProductService.php └── V5 │ ├── Posting │ └── FbsService.php │ └── ProductService.php ├── TypeCaster.php ├── Utils ├── ArrayHelper.php └── WithResolver.php └── config ├── product_validator_v1.php └── product_validator_v2.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 0.5.0 4 | 5 | - Классы сервисов, которые имеют метод `list` (CrossborderService, FboService, FbsService) являются реализациями 6 | интерфейса `HasOrdersInterface`. 7 | 8 | ## breaking changes 9 | 10 | У методов перечисленных ниже изменилась сигнатура. Метод `list` в качестве аргумента принимает только массив. 11 | 12 | - Gam6itko\OzonSeller\Service\V2\Posting\CrossborderService::list 13 | - Gam6itko\OzonSeller\Service\V2\Posting\FboService::list 14 | - Gam6itko\OzonSeller\Service\V2\Posting\FbsService::list 15 | 16 | ### before v0.5 17 | 18 | ```php 19 | use Gam6itko\OzonSeller\Service\V2\Posting\CrossborderService; 20 | 21 | $svc = new CrossborderService($config, $client); 22 | $svc->list( 23 | SortDirection::ASC, 24 | 0, 25 | 10, 26 | [ 27 | 'since' => new \DateTime('2019-01-01'), 28 | 'to' => new \DateTime('2020-01-01'), 29 | 'status' => Status::AWAITING_APPROVE, 30 | ] 31 | ); 32 | ``` 33 | 34 | ### after v0.5 35 | 36 | ```php 37 | use Gam6itko\OzonSeller\Enum\SortDirection; 38 | use Gam6itko\OzonSeller\Enum\Status; 39 | use Gam6itko\OzonSeller\Service\V2\Posting\CrossborderService; 40 | 41 | $svc = new CrossborderService($config, $client); 42 | $svc->list([ 43 | 'dir' => SortDirection::ASC, 44 | 'offset' => 0, 45 | 'limit' => 10, 46 | 'filter' => [ 47 | 'since' => new \DateTime('2019-01-01'), 48 | 'to' => new \DateTime('2020-01-01'), 49 | 'status' => Status::AWAITING_APPROVE, 50 | ] 51 | ]); 52 | 53 | // or 54 | 55 | $svc->list([ 56 | 'filter' => [ 57 | 'since' => new \DateTime('2019-01-01'), 58 | 'to' => new \DateTime('2020-01-01'), 59 | 'status' => Status::AWAITING_APPROVE, 60 | ] 61 | ]); 62 | ``` 63 | Значения по-умолчанию: 64 | 65 | ```yaml 66 | dir: 'asc' 67 | offset: 0 68 | limit: 10 69 | ``` 70 | 71 | 72 | # 0.4.1 73 | 74 | - ProductValidator для экспорта товаров V2. При создании экземпляра объекта необходимо передать вторым аргументом версию 75 | API. 76 | - CategoryService::attributeValues возвращает массив содержащий ключи [result, has_next]. 77 | 78 | # 0.4.0 79 | 80 | - При создании экземпляра Service-класса необходимо передать объект класса `Psr\Http\Client\ClientInterface`. 81 | 82 | # 0.3.0 83 | 84 | - Удалены `deprecated` методы. 85 | - declare(strict_types=1); 86 | - Поддержка сервисов API-V2. 87 | - # 17 88 | - # 18 89 | 90 | # 0.2.4 91 | 92 | - Удален класс `OrderService` т.к. сервер больше не поддерживает запросы `/v1/order` 93 | - ProductsService.updateStocks учитывает параметр `offer_id` 94 | - ProductsService.updatePrices добавлено приведение типов аргументов перед отправкой. 95 | - ProductsService.import приведение типов аргументов. 96 | 97 | # 0.2.3 98 | 99 | - [change] FboService, FbsService метод `list` изменилась последовательность параметров. Добавлена поддержка параметра 100 | filter.status 101 | 102 | # 0.2.2 103 | 104 | - классы работы с API унаследованные от `AbstractService` поддерживают логирование запросов и ответов от сервера Ozon. 105 | Необходимо передать объект LoggerInterface в метод setLogger(). 106 | 107 | # 0.2.1 108 | 109 | - [fix] CrossborderService методы `approve` и `cancel` всегда возвращали `false` 110 | 111 | # 0.2.0 112 | 113 | - Поддержка новый методов API `/v2/posting` 114 | - ProductService. Удалена функция `create`, вместо него используйте `import`. 115 | - DeliverySchema::CROSSBOARDER исправлена опечатка на DeliverySchema::CROSSBORDER -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Strizhak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ozon-seller API client 2 | [![tests](https://github.com/gam6itko/ozon-seller/actions/workflows/tests.yaml/badge.svg)](https://github.com/gam6itko/ozon-seller/actions/workflows/tests.yaml) 3 | [![Coverage Status](https://coveralls.io/repos/github/gam6itko/ozon-seller/badge.svg?branch=master)](https://coveralls.io/github/gam6itko/ozon-seller?branch=master) 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/gam6itko/ozon-seller/v)](//packagist.org/packages/gam6itko/ozon-seller) 6 | [![Total Downloads](https://poser.pugx.org/gam6itko/ozon-seller/downloads)](//packagist.org/packages/gam6itko/ozon-seller) 7 | [![Latest Unstable Version](https://poser.pugx.org/gam6itko/ozon-seller/v/unstable)](//packagist.org/packages/gam6itko/ozon-seller) 8 | [![License](https://poser.pugx.org/gam6itko/ozon-seller/license)](//packagist.org/packages/gam6itko/ozon-seller) 9 | 10 | ## Документация Ozon Api 11 | 12 | 13 | 14 | 15 | ## Установка 16 | 17 | ```shell 18 | composer require gam6itko/ozon-seller 19 | ``` 20 | 21 | Для взаимодействия библиотеки с Ozon-Api нужно дополнительно установить реализации [PSR-18: HTTP Client](https://packagist.org/providers/psr/http-client-implementation) и [PSR-17: HTTP Factories](https://packagist.org/providers/psr/http-factory-implementation). 22 | 23 | ### Использование с Symfony 24 | https://symfony.com/doc/current/components/http_client.html#psr-18-and-psr-17 25 | 26 | ```shell 27 | composer require symfony/http-client 28 | composer require nyholm/psr7 29 | ``` 30 | 31 | ```php 32 | use Gam6itko\OzonSeller\Service\V1\ProductService; 33 | use Symfony\Component\HttpClient\Psr18Client; 34 | 35 | $config = [$_SERVER['CLIENT_ID'], $_SERVER['API_KEY'], $_SERVER['API_URL']]; 36 | $client = new Psr18Client(); 37 | $svc = new ProductService($config, $client); 38 | //do stuff 39 | ``` 40 | 41 | ### Использование без Symfony 42 | 43 | ```shell 44 | composer require php-http/guzzle6-adapter 45 | ``` 46 | 47 | ```php 48 | use Gam6itko\OzonSeller\Service\V1\CategoriesService; 49 | use GuzzleHttp\Client as GuzzleClient; 50 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 51 | use Http\Factory\Guzzle\RequestFactory; 52 | use Http\Factory\Guzzle\StreamFactory; 53 | 54 | $config = [ 55 | 'clientId' => '', 56 | 'apiKey' => '', 57 | 'host' => 'http://cb-api.ozonru.me/' 58 | ]; 59 | $client = new GuzzleAdapter(new GuzzleClient()); 60 | $requestFactory = new RequestFactory(); 61 | $streamFactory = new StreamFactory(); 62 | 63 | $svc = new CategoriesService($config, $client, $requestFactory, $streamFactory); 64 | //do stuff 65 | ``` 66 | 67 | ## Реализованные методы 68 | 69 | Чтобы узнать какой класс и метод реализуют запрос на нужный URL воспользуйтесь скриптом `bin/is_realized.php` 70 | 71 | ```shell script 72 | php bin/is_realized.php | grep /v2/posting/fbs/get 73 | ``` 74 | output 75 | ```shell script 76 | /v2/posting/fbs/get: Gam6itko\OzonSeller\Service\V2\Posting\FbsService::get 77 | /v2/posting/fbs/get-by-barcode: NotRealized 78 | ``` 79 | 80 | Автор не всегда успевает добавлять реализации новых методов. 81 | Если нужного вам метода нет в библиотеке, то не стесняйтесь открыть issue или PR. 82 | 83 | ## Примеры использования 84 | Больше примеров смотрите в папке `tests/Service/` 85 | 86 | ### Categories 87 | 88 | ```php 89 | use Gam6itko\OzonSeller\Service\V1\CategoriesService; 90 | use GuzzleHttp\Client as GuzzleClient; 91 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 92 | 93 | $config = [ 94 | 'clientId' => '', 95 | 'apiKey' => '', 96 | 'host' => 'http://cb-api.ozonru.me/' //sandbox 97 | ]; 98 | $adapter = new GuzzleAdapter(new GuzzleClient()); 99 | $svc = new CategoriesService($config, $adapter); 100 | 101 | //Server Response example: https://cb-api.ozonru.me/apiref/en/#t-title_categories 102 | $categoryTree = $svc->tree(); 103 | 104 | //Server Response example: https://cb-api.ozonru.me/apiref/en/#t-title_get_categories_attributes 105 | $attributes = $svc->attributes(17038826); 106 | ``` 107 | 108 | ### Posting Crossborder 109 | 110 | #### get info 111 | 112 | `/v2/posting/crossborder/get` 113 | 114 | ```php 115 | use Gam6itko\OzonSeller\Service\V2\Posting\CrossborderService; 116 | use GuzzleHttp\Client as GuzzleClient; 117 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 118 | 119 | $config = [ 120 | 'clientId' => '', 121 | 'apiKey' => '', 122 | 'host' => 'http://cb-api.ozonru.me/' 123 | ]; 124 | $adapter = new GuzzleAdapter(new GuzzleClient()); 125 | $svc = new CrossborderService($config, $adapter); 126 | 127 | $postingNumber = '39268230-0002-3'; 128 | $orderArr = $svc->get($postingNumber); 129 | echo json_encode($orderArr); 130 | ``` 131 | 132 | ```json 133 | { 134 | "result": [ 135 | { 136 | "address": { 137 | "address_tail": "г. Москва, ул. Центральная, 1", 138 | "addressee": "Петров Иван Владимирович", 139 | "city": "Москва", 140 | "comment": "", 141 | "country": "Россия", 142 | "district": "", 143 | "phone": "+7 495 123-45-67", 144 | "region": "Москва", 145 | "zip_code": "101000" 146 | }, 147 | "auto_cancel_date": "2019-11-18T11:30:11.571Z", 148 | "cancel_reason_id": 76, 149 | "created_at": "2019-11-18T11:30:11.571Z", 150 | "customer_email": "petrov@email.com", 151 | "customer_id": 60006, 152 | "in_process_at": "2019-11-18T11:30:11.571Z", 153 | "order_id": 77712345, 154 | "order_nr": "1111444", 155 | "posting_number": "39268230-0002-3", 156 | "products": [ 157 | { 158 | "name": "Фитнес-браслет", 159 | "offer_id": "DEP-1234", 160 | "price": "1900.00", 161 | "quantity": 1, 162 | "sku": 100056 163 | } 164 | ], 165 | "shipping_provider_id": 0, 166 | "status": "awaiting_approve", 167 | "tracking_number": "" 168 | } 169 | ] 170 | } 171 | ``` 172 | 173 | ### Products 174 | 175 | #### import 176 | 177 | `/v1/product/import` 178 | 179 | ```php 180 | use Gam6itko\OzonSeller\Service\V1\ProductService; 181 | use GuzzleHttp\Client as GuzzleClient; 182 | use Http\Adapter\Guzzle6\Client as GuzzleAdapter; 183 | 184 | $config = [ 185 | 'clientId' => '', 186 | 'apiKey' => '', 187 | // use prod host by default 188 | ]; 189 | $adapter = new GuzzleAdapter(new GuzzleClient()); 190 | $svcProduct = new ProductService($config, $adapter); 191 | $product = [ 192 | 'barcode' => '8801643566784', 193 | 'description' => 'Red Samsung Galaxy S9 with 512GB', 194 | 'category_id' => 17030819, 195 | 'name' => 'Samsung Galaxy S9', 196 | 'offer_id' => 'REDSGS9-512', 197 | 'price' => '79990', 198 | 'old_price' => '89990', 199 | 'premium_price' => '75555', 200 | 'vat' => '0', 201 | 'vendor' => 'Samsung', 202 | 'vendor_code' => 'SM-G960UZPAXAA', 203 | 'height' => 77, 204 | 'depth' => 11, 205 | 'width' => 120, 206 | 'dimension_unit' => 'mm', 207 | 'weight' => 120, 208 | 'weight_unit' => 'g', 209 | 'images' => [ 210 | [ 211 | 'file_name' => 'https://ozon-st.cdn.ngenix.net/multimedia/c1200/1022555115.jpg', 212 | 'default' => true, 213 | ], 214 | [ 215 | 'file_name' => 'https://ozon-st.cdn.ngenix.net/multimedia/c1200/1022555110.jpg', 216 | 'default' => false, 217 | ], 218 | [ 219 | 'file_name' => 'https://ozon-st.cdn.ngenix.net/multimedia/c1200/1022555111.jpg', 220 | 'default' => false, 221 | ], 222 | ], 223 | 'attributes' => [ 224 | [ 225 | 'id' => 8229, 226 | 'value' => '4747', 227 | ], 228 | [ 229 | 'id' => 9048, 230 | 'value' => 'Samsung Galaxy S9', 231 | ], 232 | [ 233 | 'id' => 4742, 234 | 'value' => '512 ГБ', 235 | ], 236 | 237 | [ 238 | 'id' => 4413, 239 | 'collection' => ['1', '2', '13'], 240 | ], 241 | [ 242 | 'id' => 4018, 243 | 'complex_collection' => [ 244 | [ 245 | 'collection' => [ 246 | [ 247 | 'id' => 4068, 248 | 'value' => 'Additional video', 249 | ], 250 | [ 251 | 'id' => 4074, 252 | 'value' => '5_-NKRVn7IQ', 253 | ], 254 | ], 255 | ], 256 | [ 257 | 'collection' => [ 258 | [ 259 | 'id' => 4068, 260 | 'value' => 'Another one video', 261 | ], 262 | [ 263 | 'id' => 4074, 264 | 'value' => '5_-NKRVn7IQ', 265 | ], 266 | ], 267 | ], 268 | ], 269 | ], 270 | ], 271 | ]; 272 | 273 | $svcProduct->import($product); 274 | // or 275 | $svcProduct->import([$product, $product1, $product2, ...]); 276 | // or 277 | $res = $svcProduct->import(['items' => [$product, $product1, $product2, ...] ]); 278 | echo $res['task_id']; // save it for checking by `importInfo` 279 | ``` 280 | -------------------------------------------------------------------------------- /attr_constructor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Category attributes constructor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 24 |
25 |
26 | 30 |
31 |
32 | 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |

Category

44 |
45 |
46 |
47 |

Attributes configurator

48 |
49 |
50 |
51 |

JSON Result

52 |
53 |
54 |
55 |
56 | 57 | 58 | 99 | 100 | 101 | 102 | 153 | 154 | 155 | 598 | 599 | 600 | -------------------------------------------------------------------------------- /bin/is_realized.php: -------------------------------------------------------------------------------- 1 | null, 34 | '/v1/categories/{category_id}/attributes' => null, 35 | '/v1/category/tree' => [CategoriesService::class, 'tree'], 36 | '/v1/category/attribute' => [CategoriesService::class, 'attributes'], 37 | '/v1/product/info/description' => null, // todo 38 | '/v1/product/list/price' => [V1ProductService::class, 'price'], 39 | '/v1/product/prepayment/set' => [V1ProductService::class, 'setPrepayment'], 40 | '/v1/products/info/{product_id}' => [V1ProductService::class, 'info'], 41 | '/v1/product/info/stocks-by-warehouse/fbs' => [V1ProductService::class, 'infoStocksByWarehouseFbs'], 42 | '/v1/products/list' => null, 43 | '/v1/products/prices' => null, 44 | '/v1/products/stocks' => null, 45 | '/v1/products/update' => null, 46 | '/v1/posting/fbs/package-label/get' => [V1FbsService::class, 'packageLabelGet'], 47 | 48 | // V2 49 | '/v2/fbs/posting/delivered' => [FbsService::class.'delivered'], 50 | '/v2/fbs/posting/delivering' => [FbsService::class.'delivering'], 51 | '/v2/fbs/posting/last-mile' => [FbsService::class.'lastMile'], 52 | '/v2/fbs/posting/tracking-number/set' => [FbsService::class.'setTrackingNumber'], 53 | '/v2/posting/crossborder/cancel-reason/list' => [CrossborderService::class, 'cancelReasons'], 54 | '/v2/posting/crossborder/shipping-provider/list' => [CrossborderService::class, 'shippingProviders'], 55 | '/v2/posting/fbs/cancel-reason/list' => [FbsService::class.'cancelReasons'], 56 | '/v2/posting/fbs/product/country/list' => [FbsService::class, 'productCountryList'], 57 | '/v2/posting/fbs/product/country/set' => [FbsService::class, 'productCountrySet'], 58 | '/v2/products/info/attributes' => [V2ProductService::class, 'infoAttributes'], 59 | '/v2/returns/company/fbo' => [V2ReturnsService::class, 'company'], 60 | '/v2/returns/company/fbs' => [V2ReturnsService::class, 'company'], 61 | '/v2/posting/fbs/package-label/create' => [FbsService::class, 'packageLabelCreate'], 62 | 63 | // V3 - TODO 64 | '/v3/product/info/list' => [V3ProductService::class, 'infoList'], 65 | '/v3/product/list' => [V3ProductService::class, 'list'], 66 | 67 | // V4 68 | '/v4/product/info/prices' => [V4ProductService::class, 'infoPrices'], 69 | '/v4/product/info/stocks' => [V4ProductService::class, 'infoStocks'], 70 | 71 | // V5 72 | '/v5/fbs/posting/product/exemplar/create-or-get' => [V5FbsService::class, 'productExemplarCreateOrGet'], 73 | '/v5/fbs/posting/product/exemplar/set' => [V5FbsService::class, 'productExemplarSet'], 74 | '/v5/product/info/prices' => [V5ProductService::class, 'infoPrices'], 75 | ]; 76 | 77 | $client = new Client(); 78 | $response = $client->get(SWAGGER_URL); 79 | $json = $response->getBody()->getContents(); 80 | $swagger = json_decode($json, true); 81 | 82 | foreach ($swagger['paths'] as $path => $confArr) { 83 | if (array_key_exists($path, MAPPING)) { 84 | $classMethod = MAPPING[$path]; 85 | } else { 86 | $classMethod = findMethod($path); 87 | } 88 | 89 | echo "$path: "; 90 | 91 | // mark as deprecated 92 | $conf = reset($confArr); 93 | if (!empty($conf['deprecated']) && isDeprecated($path)) { 94 | echo "\033[01;33mdeprecated \033[0m"; 95 | } 96 | 97 | if (empty($classMethod)) { 98 | echo "\033[01;31mNotRealized\033[0m"; 99 | } else { 100 | // show class::method 101 | echo "\033[01;32m".implode('::', $classMethod)."\033[0m"; 102 | } 103 | 104 | echo PHP_EOL; 105 | } 106 | 107 | function isDeprecated(string $path): bool 108 | { 109 | if (null === ($arr = findMethod($path))) { 110 | return true; 111 | } 112 | 113 | [$class, $method] = $arr; 114 | 115 | $refClass = new ReflectionClass($class); 116 | $refMethod = $refClass->getMethod($method); 117 | if (!$docComment = $refMethod->getDocComment()) { 118 | return false; 119 | } 120 | 121 | return false !== strpos($docComment, '@deprecated'); 122 | } 123 | 124 | /** 125 | * @return array|null 126 | */ 127 | function findMethod(string $path) 128 | { 129 | $prefix = 'Gam6itko\\OzonSeller\\Service\\'; 130 | $arr = array_map('ucfirst', array_filter(explode('/', $path))); 131 | do { 132 | $key = array_shift($arr); 133 | $class = $prefix.$key.'Service'; 134 | if (class_exists($class)) { 135 | break; 136 | } 137 | 138 | $prefix .= $key.'\\'; 139 | } while (!empty($arr)); 140 | 141 | if (empty($arr)) { 142 | return null; 143 | } 144 | 145 | $arr = array_map(static function (string $string): string { 146 | return implode('', array_map('ucfirst', preg_split('/(_|-)/', $string))); 147 | }, $arr); 148 | $method = lcfirst(implode('', $arr)); 149 | 150 | if (method_exists($class, $method)) { 151 | return [$class, $method]; 152 | } 153 | 154 | return null; 155 | } 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gam6itko/ozon-seller", 3 | "description": "Ozon Seller Api for transhore sales", 4 | "keywords": ["ozon", "marketplace", "api", "russia", "seller", "crossborder"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Alexander Strizhak", 10 | "email": "gam6itko@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Gam6itko\\OzonSeller\\": "src" 16 | } 17 | }, 18 | "require": { 19 | "php": ">=7.1", 20 | "ext-json": "*", 21 | "psr/http-client": "^1.0" 22 | }, 23 | "suggest": { 24 | "php-http/guzzle6-adapter": "For use not with Symfony", 25 | "symfony/http-client": "For use with Symfony", 26 | "nyholm/psr7": "For use with Symfony" 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Gam6itko\\OzonSeller\\Tests\\": "tests" 31 | } 32 | }, 33 | "require-dev": { 34 | "ext-curl": "*", 35 | "dg/bypass-finals": "^1.2", 36 | "friendsofphp/php-cs-fixer": "^3.0", 37 | "guzzlehttp/psr7": "^1.6", 38 | "http-interop/http-factory-guzzle": "^1.0", 39 | "nyholm/psr7": "^1.3", 40 | "php-http/guzzle6-adapter": "^2.0", 41 | "phpunit/phpunit": "^7.4||^8.0||^9.0", 42 | "symfony/http-client": "^4.1||^5.1", 43 | "vimeo/psalm": "^4.0||^5.13", 44 | "vlucas/phpdotenv": "^5.3" 45 | }, 46 | "bin": [ 47 | "bin/is_realized.php" 48 | ], 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "0.9.x-dev" 52 | } 53 | }, 54 | "scripts": { 55 | "csfix": "php-cs-fixer fix", 56 | "psalm": "psalm --no-cache", 57 | "tests": "phpunit" 58 | }, 59 | "config": { 60 | "sort-packages": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Enum/ClassifierStatus.php: -------------------------------------------------------------------------------- 1 | details = $details; 16 | } 17 | 18 | public function __toString(): string 19 | { 20 | return parent::__toString().PHP_EOL.'Data: '.json_encode($this->details); 21 | } 22 | 23 | /** 24 | * @deprecated use getDetails() method 25 | */ 26 | public function getData(): ?array 27 | { 28 | return $this->details; 29 | } 30 | 31 | public function getDetails(): ?array 32 | { 33 | return $this->details; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/PostingNotFoundException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ProductValidator 13 | { 14 | private const MAX_IMAGES_COUNT = 10; 15 | 16 | /** @var array */ 17 | private $config; 18 | 19 | /** @var array */ 20 | private $requiredKeys; 21 | 22 | /** @var array */ 23 | private $optProps; 24 | 25 | /** @var array */ 26 | private $typeCast; 27 | 28 | public function __construct(string $mode = 'create', int $version = 1) 29 | { 30 | if (!in_array($mode, ['create', 'update'])) { 31 | throw new \LogicException('Mode must be in [create, update]'); 32 | } 33 | 34 | if (!in_array($version, [1, 2])) { 35 | throw new \LogicException('Version must be in [1, 2]'); 36 | } 37 | 38 | if (!file_exists($configFile = __DIR__."/config/product_validator_v{$version}.php")) { 39 | throw new \LogicException("No config found for version $version"); 40 | } 41 | 42 | $this->config = include $configFile; 43 | 44 | $this->requiredKeys = array_keys(array_filter(array_map(function ($arr) use ($mode) { 45 | return $arr['required'.ucfirst($mode)] ?? false; 46 | }, $this->config))); 47 | 48 | $this->optProps = array_filter(array_map(function ($arr) { 49 | return $arr['options'] ?? null; 50 | }, $this->config)); 51 | 52 | $this->typeCast = array_map(function ($arr) { 53 | return $arr['type']; 54 | }, $this->config); 55 | } 56 | 57 | public function validateItem(array $item) 58 | { 59 | // remove unexpected keys 60 | if ($extraKeys = array_diff(array_keys($item), array_keys($this->config))) { 61 | foreach ($extraKeys as $key) { 62 | @trigger_error("ProductValidator noticed unexpected item key '$key'"); 63 | unset($item[$key]); 64 | } 65 | } 66 | 67 | foreach ($this->requiredKeys as $key) { 68 | if (!array_key_exists($key, $item)) { 69 | throw new ProductValidatorException("Required property not defined: $key", 0, $item); 70 | } 71 | if ('string' === TypeCaster::normalizeType($this->config[$key]['type']) && '' === $item[$key]) { 72 | throw new ProductValidatorException("Empty value for property: $key", 0, $item); 73 | } 74 | } 75 | 76 | foreach ($this->optProps as $key => $options) { 77 | if (isset($item[$key]) && !in_array($item[$key], $options)) { 78 | throw new ProductValidatorException("Incorrect property value '{$item[$key]}' for `$key` key"); 79 | } 80 | } 81 | 82 | if (isset($item['images']) && count($item['images']) > self::MAX_IMAGES_COUNT) { 83 | array_splice($item['images'], self::MAX_IMAGES_COUNT); 84 | } 85 | 86 | return TypeCaster::castArr($item, $this->typeCast); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Service/AbstractService.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @psalm-type TOzonErrorData = array{ 19 | * code?: numeric, 20 | * error?: array{code?: string}, 21 | * message?: string, 22 | * details?: array, 23 | * data?: array 24 | * } 25 | */ 26 | abstract class AbstractService 27 | { 28 | /** @var array */ 29 | private $config; 30 | 31 | /** @var ClientInterface */ 32 | protected $client; 33 | 34 | /** @var RequestFactoryInterface */ 35 | protected $requestFactory; 36 | 37 | /** @var StreamFactoryInterface */ 38 | protected $streamFactory; 39 | 40 | public function __construct(array $config, ClientInterface $client, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null) 41 | { 42 | $this->parseConfig($config); 43 | $this->client = $client; 44 | 45 | // request factory 46 | if (!$requestFactory && $this->client instanceof RequestFactoryInterface) { 47 | $requestFactory = $this->client; 48 | } 49 | assert(null !== $requestFactory); 50 | $this->requestFactory = $requestFactory; 51 | 52 | // stream factory 53 | if (!$streamFactory && $this->client instanceof StreamFactoryInterface) { 54 | $streamFactory = $this->client; 55 | } 56 | assert(null !== $streamFactory); 57 | $this->streamFactory = $streamFactory; 58 | } 59 | 60 | protected function getDefaultHost(): string 61 | { 62 | return 'https://api-seller.ozon.ru'; 63 | } 64 | 65 | private function parseConfig(array $config): void 66 | { 67 | $keys = ['clientId', 'apiKey', 'host']; 68 | 69 | if (!$this->isAssoc($config)) { 70 | if (count($config) > 3) { 71 | throw new \LogicException('To many config parameters'); 72 | } 73 | $config = array_combine($keys, array_pad($config, 3, null)); 74 | } 75 | 76 | if (empty($config['clientId']) || empty($config['apiKey'])) { 77 | throw new \LogicException('Not defined mandatory config parameters `clientId` or `apiKey`'); 78 | } 79 | 80 | $config['host'] = rtrim($config['host'] ?? $this->getDefaultHost(), '/'); 81 | 82 | $this->config = ArrayHelper::pick($config, $keys); 83 | } 84 | 85 | protected function createRequest(string $method, string $uri = '', $body = null): RequestInterface 86 | { 87 | if (is_array($body)) { 88 | $body = json_encode($body); 89 | if (JSON_ERROR_NONE !== json_last_error()) { 90 | throw new \RuntimeException('json_encode error: '.json_last_error_msg()); 91 | } 92 | } 93 | 94 | $request = $this->requestFactory 95 | ->createRequest($method, $this->config['host'].$uri) 96 | ->withHeader('Client-Id', $this->config['clientId']) 97 | ->withHeader('Api-Key', $this->config['apiKey']) 98 | ->withHeader('Content-Type', 'application/json'); 99 | 100 | if ($body) { 101 | $request = $request->withBody($this->streamFactory->createStream($body)); 102 | } 103 | 104 | return $request; 105 | } 106 | 107 | /** 108 | * @param array|string|null $body 109 | */ 110 | protected function request(string $method, string $uri = '', $body = null, bool $parseResultAsJson = true, bool $returnOnlyResult = true) 111 | { 112 | try { 113 | $request = $this->createRequest($method, $uri, $body); 114 | $response = $this->client->sendRequest($request); 115 | $responseBody = $response->getBody(); 116 | 117 | // nyholm/psr7 118 | if ($response->getStatusCode() >= 400) { 119 | $this->throwOzonException($responseBody->getContents() ?: "Error status code: {$response->getStatusCode()}"); 120 | } 121 | 122 | if (!$parseResultAsJson) { 123 | return $responseBody->getContents(); 124 | } 125 | 126 | if ($responseBody->isSeekable() && 0 !== $responseBody->tell()) { 127 | $responseBody->rewind(); 128 | } 129 | 130 | $arr = json_decode($responseBody->getContents(), true); 131 | if (JSON_ERROR_NONE !== json_last_error()) { 132 | throw new \RuntimeException('Invalid json response: '.$arr); 133 | } 134 | 135 | if (isset($arr['result']) && $returnOnlyResult) { 136 | return $arr['result']; 137 | } 138 | 139 | return $arr; 140 | } catch (RequestExceptionInterface $requestException) { 141 | // guzzle 142 | if (method_exists($requestException, 'getResponse')) { 143 | $response = $requestException->getResponse(); 144 | if (method_exists($response, 'getBody')) { 145 | $contents = $response->getBody()->getContents(); 146 | $this->throwOzonException($contents ?: "Error status code: {$response->getStatusCode()}"); 147 | } 148 | } 149 | throw $requestException; 150 | } 151 | } 152 | 153 | protected function throwOzonException(string $responseBodyContents): void 154 | { 155 | /** @var TOzonErrorData $errorData */ 156 | $errorData = json_decode($responseBodyContents, true); 157 | if (JSON_ERROR_NONE !== json_last_error()) { 158 | throw new OzonSellerException($responseBodyContents); 159 | } 160 | 161 | if (!isset($errorData['error']) || empty($errorData['error']['code'])) { 162 | throw new OzonSellerException($errorData['message'] ?? 'Ozon error', (int) ($errorData['code'] ?? 0), $errorData['details'] ?? []); 163 | } 164 | 165 | if (!class_exists($className = $this->getExceptionClassByName($errorData['error']['code']))) { 166 | throw new OzonSellerException($responseBodyContents); 167 | } 168 | 169 | $errorData = array_merge([ 170 | 'message' => '', 171 | 'data' => [], 172 | ], $errorData['error']); 173 | 174 | $refClass = new \ReflectionClass($className); 175 | /** @var \Throwable $instance */ 176 | $instance = $refClass->newInstance($errorData['message'], 0, $errorData['data'] ?? []); 177 | throw $instance; 178 | } 179 | 180 | private function getExceptionClassByName(string $code): string 181 | { 182 | $parts = array_filter(explode('_', strtolower($code))); 183 | // 'error' будет заменен на Exception 184 | if ('error' === end($parts)) { 185 | unset($parts[key($parts)]); 186 | } 187 | $parts = array_map('ucfirst', $parts); 188 | $name = implode('', $parts); 189 | 190 | return "Gam6itko\\OzonSeller\\Exception\\{$name}Exception"; 191 | } 192 | 193 | protected function ensureCollection(array $arr) 194 | { 195 | return $this->isAssoc($arr) ? [$arr] : $arr; 196 | } 197 | 198 | protected function isAssoc(array $arr): bool 199 | { 200 | return array_keys($arr) !== range(0, count($arr) - 1); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Service/GetOrderInterface.php: -------------------------------------------------------------------------------- 1 | 'string', 14 | 'car_number' => 'string', 15 | 'driver_name' => 'string', 16 | 'driver_patronymic' => 'string', 17 | 'driver_surname' => 'string', 18 | 'end_unloading_time' => 'string', 19 | 'is_regular_pass' => 'boolean', 20 | 'start_unloading_time' => 'string', 21 | 'telephone' => 'string', 22 | 'trailer_number' => 'string', 23 | 'unload_date' => 'string', 24 | ]; 25 | 26 | public function create(array $data) 27 | { 28 | ArrayHelper::pick($data, array_keys(self::CONF)); 29 | TypeCaster::castArr($data, self::CONF); 30 | 31 | return $this->request('POST', '/pass/create', $data, true, false); 32 | } 33 | 34 | public function getLast() 35 | { 36 | return $this->request('POST', '/pass/get/last', '{}', true, false); 37 | } 38 | 39 | public function update(array $data) 40 | { 41 | ArrayHelper::pick($data, array_keys(self::CONF)); 42 | TypeCaster::castArr($data, self::CONF); 43 | 44 | return $this->request('POST', '/pass/update', $data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Service/V1/ActionsService.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ActionsService extends AbstractService 16 | { 17 | private $path = '/v1/actions'; 18 | 19 | protected function getDefaultHost(): string 20 | { 21 | return 'https://seller-api.ozon.ru/'; 22 | } 23 | 24 | /** 25 | * Promotional offers list. 26 | * 27 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_action_available 28 | */ 29 | public function list(): array 30 | { 31 | return $this->request('GET', $this->path); 32 | } 33 | 34 | /** 35 | * List of products which can participate in the promotional offer. 36 | * 37 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_action_available_products 38 | */ 39 | public function candidates(int $actionId, int $offset = 0, int $limit = 10): array 40 | { 41 | $body = [ 42 | 'action_id' => $actionId, 43 | 'offset' => $offset, 44 | 'limit' => $limit, 45 | ]; 46 | 47 | return $this->request('POST', "{$this->path}/candidates", $body); 48 | } 49 | 50 | /** 51 | * List of products which participate in the promotional offer. 52 | * 53 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_action_products 54 | */ 55 | public function products(int $actionId, int $offset = 0, int $limit = 10) 56 | { 57 | $body = [ 58 | 'action_id' => $actionId, 59 | 'offset' => $offset, 60 | 'limit' => $limit, 61 | ]; 62 | 63 | return $this->request('POST', "{$this->path}/products", $body); 64 | } 65 | 66 | /** 67 | * Add product to the promotional offer. 68 | * 69 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_action_add_products 70 | */ 71 | public function productsActivate(int $actionId, array $products): array 72 | { 73 | $products = $this->ensureCollection($products); 74 | foreach ($products as &$p) { 75 | $p = ArrayHelper::pick($p, ['product_id', 'action_price']); 76 | } 77 | unset($p); 78 | 79 | $body = [ 80 | 'action_id' => $actionId, 81 | 'products' => $products, 82 | ]; 83 | 84 | return $this->request('POST', "{$this->path}/products/activate", $body); 85 | } 86 | 87 | /** 88 | * This method allows to delete products from the promotional offer. 89 | * 90 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_action_add_products 91 | */ 92 | public function productsDeactivate(int $actionId, array $productIds): array 93 | { 94 | $body = [ 95 | 'action_id' => $actionId, 96 | 'product_ids' => $productIds, 97 | ]; 98 | 99 | return $this->request('POST', "{$this->path}/products/deactivate", $body); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Service/V1/AnalyticsService.php: -------------------------------------------------------------------------------- 1 | $dateFrom->format('Y-m-d'), 30 | 'date_to' => $dateTo->format('Y-m-d'), 31 | 'dimension' => $dimension, 32 | 'metrics' => $metrics, 33 | 'offset' => $offset, 34 | 'limit' => $limit, 35 | 'filters' => $filters, 36 | 'sort' => $sort, 37 | ]; 38 | 39 | return $this->request('POST', "{$this->path}/data", $body); 40 | } 41 | 42 | /** 43 | * Report on stocks and products movement at Ozon warehouses.. 44 | * 45 | * @see https://docs.ozon.ru/api/seller/en/#operation/AnalyticsAPI_AnalyticsGetStockOnWarehouses 46 | */ 47 | public function stockOnWarehouses(int $offset = 0, int $limit = 10): array 48 | { 49 | $body = [ 50 | 'offset' => $offset, 51 | 'limit' => $limit, 52 | ]; 53 | 54 | return $this->request( 55 | 'POST', 56 | "{$this->path}/stock_on_warehouses", 57 | $body 58 | ); 59 | } 60 | 61 | /** 62 | * Method for getting a turnover report (FBO) by category for 15 days. 63 | * 64 | * @see https://docs.ozon.ru/api/seller/en/#operation/AnalyticsAPI_AnalyticsItemTurnoverDataV3 65 | */ 66 | public function itemTurnover(\DateTimeInterface $dateFrom): array 67 | { 68 | $body = [ 69 | 'date_from' => $dateFrom->format('Y-m-d'), 70 | ]; 71 | 72 | return $this->request('POST', "{$this->path}/item_turnover", $body); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Service/V1/BrandService.php: -------------------------------------------------------------------------------- 1 | 1, 'page_size' => 100], 24 | ArrayHelper::pick($query, ['page', 'page_size']) 25 | ); 26 | 27 | return $this->request('POST', "{$this->path}/company-certification/list", $pagination); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Service/V1/CategoriesService.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CategoriesService extends AbstractService 15 | { 16 | /** 17 | * Receive the list of all available item categories. 18 | * 19 | * @deprecated use V2\CategoryService::tree 20 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_categories_tree 21 | * 22 | * @param 'EN'|'RU' $language 23 | * 24 | * @return array 25 | */ 26 | public function tree(?int $categoryId = null, string $language = 'RU') 27 | { 28 | $query = array_filter([ 29 | 'category_id' => $categoryId, 30 | 'language' => strtoupper($language), 31 | ]); 32 | 33 | return $this->request('POST', '/v1/category/tree', $query); 34 | } 35 | 36 | /** 37 | * Receive the attributes list from the product page for a specified category. 38 | * 39 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_categories_attributes 40 | * 41 | * @param 'EN'|'RU' $language 42 | * 43 | * @return mixed|\Psr\Http\Message\ResponseInterface 44 | */ 45 | public function attributes(int $categoryId, string $language = 'RU', array $query = []) 46 | { 47 | $query = ArrayHelper::pick($query, ['attribute_type']); 48 | $query = TypeCaster::castArr($query, ['attribute_type' => 'str']); 49 | $query = array_merge([ 50 | 'category_id' => $categoryId, 51 | 'language' => strtoupper($language), 52 | ], $query); 53 | 54 | return $this->request('POST', '/v1/category/attribute', $query); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Service/V1/ChatService.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @psalm-type TListQuery = array{ 15 | * chat_id_list?: list, 16 | * page?: int, 17 | * page_size?: int, 18 | * } 19 | * @psalm-type THistoryQuery = array{ 20 | * from_message_id?: int, 21 | * limit?: int, 22 | * } 23 | */ 24 | class ChatService extends AbstractService 25 | { 26 | /** 27 | * Retrieves a list of chats in which a seller participates. 28 | * 29 | * @param TListQuery $query 30 | * 31 | * @return array 32 | */ 33 | public function list(array $query = []) 34 | { 35 | $query = ArrayHelper::pick($query, ['chat_id_list', 'page', 'page_size']); 36 | $query = TypeCaster::castArr($query, ['page' => 'int', 'page_size' => 'int']); 37 | 38 | return $this->request('POST', '/v1/chat/list', $query ?: '{}'); 39 | } 40 | 41 | /** 42 | * Retreives message history in a chat. 43 | * 44 | * @param THistoryQuery $query 45 | * 46 | * @return array 47 | */ 48 | public function history(string $chatId, array $query = []) 49 | { 50 | $query = ArrayHelper::pick($query, ['from_message_id', 'limit']); 51 | 52 | $query['chat_id'] = $chatId; 53 | 54 | return $this->request('POST', '/v1/chat/history', $query); 55 | } 56 | 57 | /** 58 | * Sends a message in an existing chat with a customer. 59 | */ 60 | public function sendMessage(string $chatId, string $text): bool 61 | { 62 | $arr = [ 63 | 'chat_id' => $chatId, 64 | 'text' => $text, 65 | ]; 66 | 67 | $response = $this->request('POST', '/v1/chat/send/message', $arr); 68 | 69 | return 'success' === $response; 70 | } 71 | 72 | /** 73 | * @see https://api-seller.ozon.ru/apiref/en/#t-title_post_sendfile 74 | */ 75 | public function sendFile(string $chatId, \SplFileInfo $file) 76 | { 77 | $arr = [ 78 | 'chat_id' => $chatId, 79 | 'base64_content' => base64_encode(file_get_contents($file->getPathname())), 80 | 'name' => $file->getBasename(), 81 | ]; 82 | $response = $this->request('POST', '/v1/chat/send/file', $arr); 83 | 84 | return 'success' === $response; 85 | } 86 | 87 | /** 88 | * @see https://api-seller.ozon.ru/apiref/ru/#t-title_post_chatstart 89 | * 90 | * @return string Chat ID 91 | */ 92 | public function start(string $postingNumber): string 93 | { 94 | $arr = [ 95 | 'posting_number' => $postingNumber, 96 | ]; 97 | 98 | return $this->request('POST', '/v1/chat/start', $arr)['chat_id']; 99 | } 100 | 101 | public function updates(string $chatId, string $fromMessageId, int $limit = 100) 102 | { 103 | $arr = [ 104 | 'chat_id' => $chatId, 105 | 'from_message_id' => $fromMessageId, 106 | 'limit' => $limit, 107 | ]; 108 | 109 | return $this->request('POST', '/v1/chat/updates', $arr); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Service/V1/FinanceService.php: -------------------------------------------------------------------------------- 1 | request('POST', '/v1/finance/realization', $query); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Service/V1/Posting/FbsService.php: -------------------------------------------------------------------------------- 1 | $taskId, 21 | ]; 22 | return $this->request('POST', "{$this->path}/package-label/get", $body); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Service/V1/ProductService.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @psalm-type TInfoByQuery = array{ 16 | * product_id?: int, 17 | * sku?: int, 18 | * offer_id?: string 19 | * } 20 | * @psalm-type TPagination = array{page?: int, page_size?: int} 21 | */ 22 | class ProductService extends AbstractService 23 | { 24 | /** 25 | * Automatically determines a product category for a product. 26 | * 27 | * @param array $income Single product structure or array of structures 28 | * 29 | * @return array 30 | * 31 | * @deprecated 32 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_product_classifier 33 | */ 34 | public function classify(array $income) 35 | { 36 | if (!array_key_exists('products', $income)) { 37 | $income = $this->ensureCollection($income); 38 | $income = ['products' => $income]; 39 | } 40 | 41 | $income = ArrayHelper::pick($income, ['products']); 42 | foreach ($income['products'] as &$p) { 43 | $p = ArrayHelper::pick($p, [ 44 | 'offer_id', 45 | 'shop_category_full_path', 46 | 'shop_category', 47 | 'shop_category_id', 48 | 'vendor', 49 | 'model', 50 | 'name', 51 | 'price', 52 | 'offer_url', 53 | 'img_url', 54 | 'vendor_code', 55 | 'barcode', 56 | ]); 57 | } 58 | 59 | return $this->request('POST', '/v1/product/classify', $income); 60 | } 61 | 62 | /** 63 | * Creates product page in our system. 64 | * 65 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_create 66 | * 67 | * @param array $income Single item structure or array of items 68 | * 69 | * @return array 70 | */ 71 | public function import(array $income, bool $validateBeforeSend = true) 72 | { 73 | if (!array_key_exists('items', $income)) { 74 | $income = $this->ensureCollection($income); 75 | $income = ['items' => $income]; 76 | } 77 | 78 | $income = ArrayHelper::pick($income, ['items']); 79 | 80 | if ($validateBeforeSend) { 81 | $pv = new ProductValidator('create'); 82 | foreach ($income['items'] as &$item) { 83 | $item = $pv->validateItem($item); 84 | } 85 | } 86 | 87 | // cast attributes types. 88 | foreach ($income['items'] as &$item) { 89 | if (isset($item['attributes']) && is_array($item['attributes']) && count($item['attributes']) > 0) { 90 | foreach ($item['attributes'] as &$attribute) { 91 | $attribute = TypeCaster::castArr($attribute, ['value' => 'str']); 92 | if (isset($item['collection']) && is_array($attribute['collection']) && count($attribute['collection']) > 0) { 93 | foreach ($attribute['collection'] as &$collectionItem) { 94 | $collectionItem = (string) $collectionItem; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | return $this->request('POST', '/v1/product/import', $income); 102 | } 103 | 104 | /** 105 | * @param array $income Single item structure or array of item 106 | * 107 | * @return array|string 108 | */ 109 | public function importBySku(array $income) 110 | { 111 | if (!array_key_exists('items', $income)) { 112 | $income = $this->ensureCollection($income); 113 | $income = ['items' => $income]; 114 | } 115 | 116 | $income = ArrayHelper::pick($income, ['items']); 117 | foreach ($income['items'] as &$item) { 118 | $item = TypeCaster::castArr( 119 | ArrayHelper::pick($item, ['sku', 'name', 'offer_id', 'price', 'old_price', 'premium_price', 'vat']), 120 | [ 121 | 'offer_id' => 'str', 122 | 'price' => 'str', 123 | 'old_price' => 'str', 124 | 'premium_price' => 'str', 125 | 'vat' => 'str', 126 | ] 127 | ); 128 | } 129 | 130 | return $this->request('POST', '/v1/product/import-by-sku', $income); 131 | } 132 | 133 | /** 134 | * Product creation status. 135 | * 136 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_create_status 137 | * 138 | * @param int $taskId Product import task id 139 | * 140 | * @return array 141 | */ 142 | public function importInfo(int $taskId) 143 | { 144 | $query = ['task_id' => $taskId]; 145 | 146 | return $this->request('POST', '/v1/product/import/info', $query); 147 | } 148 | 149 | /** 150 | * @param int $productId Id of product in Ozon system 151 | * 152 | * @return array 153 | * 154 | * @deprecated use V2\ProductService::info 155 | * 156 | * Receive product info 157 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_products_info 158 | */ 159 | public function info(int $productId) 160 | { 161 | $query = ['product_id' => $productId]; 162 | $query = TypeCaster::castArr($query, ['product_id' => 'int']); 163 | 164 | return $this->request('POST', '/v1/product/info', $query); 165 | } 166 | 167 | /** 168 | * Receive product info. 169 | * 170 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_products_info 171 | * 172 | * @param TInfoByQuery $query 173 | * 174 | * @return array 175 | */ 176 | public function infoBy(array $query) 177 | { 178 | $query = ArrayHelper::pick($query, ['product_id', 'sku', 'offer_id']); 179 | $query = TypeCaster::castArr($query, ['product_id' => 'int', 'sku' => 'int', 'offer_id' => 'str']); 180 | 181 | return $this->request('POST', '/v1/product/info', $query); 182 | } 183 | 184 | /** 185 | * @param TPagination $pagination 186 | * 187 | * @return array 188 | * 189 | * @deprecated use V4\ProductService::infoPrices 190 | * 191 | * Receive products prices info 192 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_get_product_info_prices 193 | */ 194 | public function infoPrices(array $pagination = []) 195 | { 196 | $pagination = array_merge( 197 | ['page' => 1, 'page_size' => 100], 198 | ArrayHelper::pick($pagination, ['page', 'page_size']) 199 | ); 200 | 201 | return $this->request('POST', '/v1/product/info/prices', $pagination); 202 | } 203 | 204 | /** 205 | * @param TPagination $pagination 206 | * 207 | * @return array 208 | * 209 | * @deprecated use V3\ProductService::infoStocks 210 | * 211 | * Receive products stocks info 212 | * @deprecated 213 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_get_product_info_stocks 214 | */ 215 | public function infoStocks(array $pagination = []) 216 | { 217 | $pagination = array_merge( 218 | ['page' => 1, 'page_size' => 100], 219 | ArrayHelper::pick($pagination, ['page', 'page_size']) 220 | ); 221 | 222 | return $this->request('POST', '/v1/product/info/stocks', $pagination); 223 | } 224 | 225 | /** 226 | * @deprecated use V2\ProductService::list 227 | * 228 | * Receive the list of products. 229 | * 230 | * query['filter'] 231 | * [offer_id] string|int|array 232 | * [product_id] string|int|array, 233 | * [visibility] string 234 | * [page] int 235 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_products_list 236 | */ 237 | public function list(array $query = [], array $pagination = []): array 238 | { 239 | $filterKeys = ['offer_id', 'product_id', 'visibility']; 240 | 241 | if (!isset($query['filter']) && array_intersect($filterKeys, array_keys($query))) { 242 | $query = ['filter' => $query]; 243 | } 244 | 245 | if (isset($query['filter'])) { 246 | $query['filter'] = ArrayHelper::pick($query['filter'], $filterKeys); 247 | // normalize offer_id data 248 | if (isset($query['filter']['offer_id'])) { 249 | $query['filter']['offer_id'] = array_map('strval', (array) $query['filter']['offer_id']); 250 | } 251 | // normalize product_id data 252 | if (isset($query['filter']['product_id'])) { 253 | $query['filter']['product_id'] = array_map('intval', (array) $query['filter']['product_id']); 254 | } 255 | } 256 | 257 | $query = array_merge($pagination, $query); 258 | $query = array_merge(['page' => 1, 'page_size' => 10], $query); 259 | 260 | return $this->request('POST', '/v1/product/list', array_filter($query)); 261 | } 262 | 263 | /** 264 | * Update the price for one or multiple products. 265 | * 266 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_prices 267 | * 268 | * @return array 269 | */ 270 | public function importPrices(array $input) 271 | { 272 | if (empty($input)) { 273 | throw new \InvalidArgumentException('Empty prices data'); 274 | } 275 | 276 | if ($this->isAssoc($input) && !isset($input['prices'])) {// if it one price 277 | $input = ['prices' => [$input]]; 278 | } else { 279 | if (!$this->isAssoc($input)) {// if it plain array on prices 280 | $input = ['prices' => $input]; 281 | } 282 | } 283 | 284 | if (!isset($input['prices'])) { 285 | throw new \InvalidArgumentException(); 286 | } 287 | 288 | foreach ($input['prices'] as $i => &$p) { 289 | if (!$p = ArrayHelper::pick($p, [ 290 | 'product_id', 291 | 'offer_id', 292 | 'price', 293 | 'old_price', 294 | 'premium_price', 295 | 'min_price', 296 | ])) { 297 | throw new \InvalidArgumentException('Invalid price data at index '.$i); 298 | } 299 | 300 | // old_price must be greater than price 301 | if (!empty($p['old_price']) && !empty($p['price']) && (float) $p['price'] > (float) $p['old_price']) { 302 | @trigger_error('`old_price` must be greater than `price`', E_USER_WARNING); 303 | $p['old_price'] = 0; 304 | } 305 | 306 | $p = TypeCaster::castArr( 307 | $p, 308 | [ 309 | 'product_id' => 'int', 310 | 'offer_id' => 'str', 311 | 'price' => 'str', 312 | 'old_price' => 'str', 313 | 'premium_price' => 'str', 314 | 'min_price' => 'str', 315 | ] 316 | ); 317 | } 318 | 319 | return $this->request('POST', '/v1/product/import/prices', $input); 320 | } 321 | 322 | /** 323 | * @return array 324 | * 325 | * @deprecated use V2\ProductService::importStocks 326 | * 327 | * Update product stocks 328 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_stocks 329 | */ 330 | public function importStocks(array $input) 331 | { 332 | if (empty($input)) { 333 | throw new \InvalidArgumentException('Empty stocks data'); 334 | } 335 | 336 | if ($this->isAssoc($input) && !isset($input['stocks'])) {// if its one price 337 | $input = ['stocks' => [$input]]; 338 | } else { 339 | if (!$this->isAssoc($input)) {// if it plain arrays on prices 340 | $input = ['stocks' => $input]; 341 | } 342 | } 343 | 344 | if (!isset($input['stocks'])) { 345 | throw new \InvalidArgumentException(); 346 | } 347 | 348 | foreach ($input['stocks'] as $i => &$s) { 349 | if (!$s = ArrayHelper::pick($s, ['product_id', 'offer_id', 'stock'])) { 350 | throw new \InvalidArgumentException('Invalid stock data at index '.$i); 351 | } 352 | 353 | $s = TypeCaster::castArr( 354 | $s, 355 | [ 356 | 'product_id' => 'int', 357 | 'offer_id' => 'str', 358 | 'stock' => 'int', 359 | ] 360 | ); 361 | } 362 | 363 | return $this->request('POST', '/v1/product/import/stocks', $input); 364 | } 365 | 366 | /** 367 | * Change the product info. Please note, that you cannot update price and stocks. 368 | * 369 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_prices 370 | * 371 | * @param array $product Product structure 372 | * @param bool $validate Perform validation before send 373 | * 374 | * @return array 375 | * 376 | * @todo return bool 377 | */ 378 | public function update(array $product, bool $validate = true) 379 | { 380 | if ($validate) { 381 | $pv = new ProductValidator('update'); 382 | $product = $pv->validateItem($product); 383 | } 384 | 385 | return $this->request('POST', '/v1/product/update', $product); 386 | } 387 | 388 | /** 389 | * Mark the product as in stock. 390 | * 391 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_activate 392 | * 393 | * @return bool success 394 | * 395 | * @deprecated 396 | */ 397 | public function activate(int $productId): bool 398 | { 399 | $response = $this->request('POST', '/v1/product/activate', ['product_id' => $productId]); 400 | 401 | return 'success' === $response; 402 | } 403 | 404 | /** 405 | * Mark the product as not in stock. 406 | * 407 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_post_products_deactivate 408 | * 409 | * @param int $productId Ozon Product Id 410 | * 411 | * @return bool success 412 | * 413 | * @deprecated 414 | */ 415 | public function deactivate(int $productId): bool 416 | { 417 | $response = $this->request('POST', '/v1/product/deactivate', ['product_id' => $productId]); 418 | 419 | return 'success' === $response; 420 | } 421 | 422 | /** 423 | * This method allows you to remove product in some cases: [product must not have active stocks, product should not have any sales]. 424 | * 425 | * @return bool deleted 426 | * 427 | * @deprecated 428 | */ 429 | public function delete(int $productId, ?string $offerId = null) 430 | { 431 | $query = array_filter([ 432 | 'product_id' => $productId, 433 | 'offer_id' => $offerId, 434 | ]); 435 | $response = $this->request('POST', '/v1/product/delete', $query); 436 | 437 | return 'deleted' === $response; 438 | } 439 | 440 | /** 441 | * @see https://github.com/gam6itko/ozon-seller/issues/6 442 | * 443 | * @param array $filter ["offer_id": [], "product_id": [], "visibility": "ALL"] 444 | * @param array $pagination [page, page_size] 445 | * 446 | * @todo filter not works 447 | * 448 | * @return array 449 | */ 450 | public function price(array $filter = [], array $pagination = []) 451 | { 452 | $filter = ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']); 453 | $pagination = ArrayHelper::pick($pagination, ['page', 'page_size']); 454 | $body = array_merge($pagination, [ 455 | 'filter' => $filter, 456 | ]); 457 | 458 | return $this->request('POST', '/v1/product/list/price', $body); 459 | } 460 | 461 | /** 462 | * @see https://cb-api.ozonru.me/apiref/en/#t-prepayment_set 463 | * 464 | * @param array $data ['is_prepayment', 'offers_ids', 'products_ids'] 465 | * 466 | * @return array|string 467 | */ 468 | public function setPrepayment(array $data) 469 | { 470 | $data = ArrayHelper::pick($data, ['is_prepayment', 'offers_ids', 'products_ids']); 471 | 472 | return $this->request('POST', '/v1/product/prepayment/set', $data); 473 | } 474 | 475 | /** 476 | * Place product to archive. 477 | * 478 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_DeleteProducts 479 | * 480 | * @param int|string|array $productId 481 | */ 482 | public function archive($productId): bool 483 | { 484 | if (!is_array($productId)) { 485 | $productId = [$productId]; 486 | } 487 | $query = ['product_id' => $productId]; 488 | 489 | return $this->request('POST', '/v1/product/archive', $query); 490 | } 491 | 492 | /** 493 | * Returns product from archive to store. 494 | * 495 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_ProductUnarchive 496 | * 497 | * @param int|string|array $productId 498 | */ 499 | public function unarchive($productId): bool 500 | { 501 | if (!is_array($productId)) { 502 | $productId = [$productId]; 503 | } 504 | $query = ['product_id' => $productId]; 505 | 506 | return $this->request('POST', '/v1/product/unarchive', $query); 507 | } 508 | 509 | /** 510 | * @see https://docs.ozon.ru/api/seller#/certificate/accordance-types-get 511 | */ 512 | public function certificateAccordanceTypes() 513 | { 514 | return $this->request('GET', '/v1/product/certificate/accordance-types', '{}'); 515 | } 516 | 517 | /** 518 | * @see https://docs.ozon.ru/api/seller#/certificate/bind-post 519 | */ 520 | public function certificateBind(int $certificateId, array $itemIds): bool 521 | { 522 | $body = [ 523 | 'certificate_id' => $certificateId, 524 | 'item_id' => $itemIds, 525 | ]; 526 | 527 | return $this->request('POST', '/v1/product/certificate/accordance-types', $body); 528 | } 529 | 530 | /** 531 | * @see https://docs.ozon.ru/api/seller#/certificate/create-post 532 | */ 533 | public function certificateCreate(array $data): int 534 | { 535 | return $this->request('POST', '/v1/product/certificate/create', $data)['id']; 536 | } 537 | 538 | /** 539 | * @see https://docs.ozon.ru/api/seller#/certificate/types-get 540 | */ 541 | public function certificateTypes(): array 542 | { 543 | return $this->request('GET', '/v1/product/certificate/types'); 544 | } 545 | 546 | /** 547 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_ProductImportPictures 548 | */ 549 | public function picturesImport(array $query): array 550 | { 551 | $query = ArrayHelper::pick($query, ['color_image', 'images', 'images360', 'primary_image', 'product_id']); 552 | $query = TypeCaster::castArr($query, [ 553 | 'color_image' => 'str', 554 | 'images' => 'arrOfStr', 555 | 'images360' => 'arrOfStr', 556 | 'primary_image' => 'str', 557 | 'product_id' => 'int', 558 | ]); 559 | 560 | return $this->request('POST', '/v1/product/pictures/import', $query); 561 | } 562 | 563 | /** 564 | * @param string[]|string $productId 565 | */ 566 | public function picturesInfo($productId): array 567 | { 568 | return $this->request('POST', '/v1/product/pictures/info', [ 569 | 'product_id' => TypeCaster::cast($productId, 'arrOfStr'), 570 | ]); 571 | } 572 | 573 | /** 574 | * Receive product content rating by sku. 575 | * 576 | * @see https://seller-edu.ozon.ru/docs/work-with-goods/content-rating.html 577 | */ 578 | public function ratingBySku(array $query): array 579 | { 580 | $query = ArrayHelper::pick($query, ['skus']); 581 | $query = TypeCaster::castArr($query, [ 582 | 'skus' => 'arrOfInt', 583 | ]); 584 | 585 | return $this->request('POST', '/v1/product/rating-by-sku', $query); 586 | } 587 | 588 | /** 589 | * Receive product description. 590 | * 591 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_GetProductInfoDescription 592 | * 593 | * @param array $query ['product_id', 'offer_id'] 594 | */ 595 | public function infoDescription(array $query): array 596 | { 597 | $query = ArrayHelper::pick($query, ['product_id', 'offer_id']); 598 | $query = TypeCaster::castArr($query, ['product_id' => 'int', 'offer_id' => 'str']); 599 | 600 | return $this->request('POST', '/v1/product/info/description', $query); 601 | } 602 | 603 | /** 604 | * Receive stocks in seller's warehouses (FBS и rFBS). 605 | * fbs-sku param is deprecated since August 15, 2023. 606 | * 607 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_ProductStocksByWarehouseFbs 608 | * 609 | * @psalm-type TStocksQuery = array{ 610 | * sku?: int[], 611 | * fbs_sku?: int[], 612 | * } 613 | * @psalm-type TStocks = array{ 614 | * sku: int, 615 | * fbs_sku?: int, 616 | * present: int, 617 | * product_id: int, 618 | * reserved: int, 619 | * warehouse_id: int, 620 | * warehouse_name: string, 621 | * } 622 | * 623 | * @param TStocksQuery $query 624 | * 625 | * @return list 626 | */ 627 | public function infoStocksByWarehouseFbs(array $query): array 628 | { 629 | $query = ArrayHelper::pick($query, ['sku', 'fbs_sku']); 630 | $query = TypeCaster::castArr($query, ['sku' => 'arrayOfString', 'fbs_sku' => 'arrayOfString']); 631 | 632 | return $this->request('POST', '/v1/product/info/stocks-by-warehouse/fbs', $query); 633 | } 634 | } 635 | -------------------------------------------------------------------------------- /src/Service/V1/ReportService.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ReportService extends AbstractService 19 | { 20 | /** 21 | * Returns a list of reports which were previously generated by Seller. 22 | * 23 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_post_reportlist 24 | * 25 | * @return array|string 26 | */ 27 | public function list(array $query) 28 | { 29 | $query = ArrayHelper::pick($query, ['page', 'page_size', 'report_type']); 30 | 31 | return $this->request('POST', '/v1/report/list', $query); 32 | } 33 | 34 | /** 35 | * Get report info by unique ID. 36 | * 37 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_post_reportinfo 38 | * 39 | * @return array|string 40 | */ 41 | public function info(?string $code = null) 42 | { 43 | $query = array_filter(['code' => $code]); 44 | 45 | return $this->request('POST', '/v1/report/info', $query); 46 | } 47 | 48 | /** 49 | * Returns products reports, which is also available in Seller Center. 50 | * 51 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_post_reportproducts 52 | * 53 | * @param array $query ['offer_id', 'search', 'sku', 'visibility'] 54 | * 55 | * @return array 56 | */ 57 | public function products(array $query = []) 58 | { 59 | $query = ArrayHelper::pick($query, ['offer_id', 'search', 'sku', 'visibility']); 60 | $query = array_filter($query); 61 | 62 | return $this->request('POST', '/v1/report/products/create', $query); 63 | } 64 | 65 | /** 66 | * Returns products reports, which is also available in Seller Center. 67 | * 68 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_post_reporttransactions 69 | * 70 | * @return array|string 71 | */ 72 | public function transaction(\DateTimeInterface $dateFrom, \DateTimeInterface $dateTo, ?string $search = null, string $transactionType = TransactionType::ALL) 73 | { 74 | $query = array_filter([ 75 | 'date_from' => $dateFrom->format('Y-m-d'), 76 | 'date_to' => $dateTo->format('Y-m-d'), 77 | 'search' => $search, 78 | 'transaction_type' => $transactionType, 79 | ]); 80 | 81 | return $this->request('POST', '/v1/report/transactions/create', $query); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Service/V1/ReturnService.php: -------------------------------------------------------------------------------- 1 | , 19 | * product_name?: string, 20 | * offer_id?: string, 21 | * visual_status_name?: string, 22 | * warehouse_id?: int, 23 | * barcode?: string, 24 | * return_schema?: string 25 | * } 26 | * 27 | * @psalm-type TReturnListRequestLimit = int 28 | * 29 | * @psalm-type TReturnListRequestLastId = int 30 | * 31 | * @psalm-type TReturnListRequestResponse = array{ 32 | * returns: array, 33 | * has_next: bool 34 | * } 35 | */ 36 | class ReturnService extends AbstractService 37 | { 38 | private $path = '/v1/returns'; 39 | 40 | /** 41 | * @see https://docs.ozon.ru/api/seller/#operation/returnsList 42 | * 43 | * @param TReturnListRequestFilter $filter 44 | * @param TReturnListRequestLastId $lastId 45 | * @param TReturnListRequestLimit $limit 46 | * 47 | * @return TReturnListRequestResponse 48 | */ 49 | public function list(array $filter, int $lastId = 0, int $limit = 100): array 50 | { 51 | assert($limit > 0 && $limit <= 500); 52 | 53 | $body = [ 54 | 'filter' => ArrayHelper::pick($filter, [ 55 | 'logistic_return_date', 'storage_tariffication_start_date', 'visual_status_change_moment', 56 | 'order_id', 'posting_numbers', 'product_name', 'offer_id', 'visual_status_name', 'warehouse_id', 57 | 'barcode', 'return_schema' 58 | ]), 59 | 'last_id' => $lastId, 60 | 'limit' => $limit, 61 | ]; 62 | 63 | return $this->request('POST', "{$this->path}/list", $body); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Service/V1/WarehouseService.php: -------------------------------------------------------------------------------- 1 | request('POST', "{$this->path}/list"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Service/V2/AnalyticsService.php: -------------------------------------------------------------------------------- 1 | $offset, 22 | 'limit' => $limit, 23 | 'warehouse_type' => $warehouse_type 24 | ]; 25 | 26 | return $this->request( 27 | 'POST', 28 | "{$this->path}/stock_on_warehouses", 29 | $body 30 | ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/V2/CategoryService.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @psalm-type TAttributeQuery = array{attribute_type?: string, language?: string} 15 | * @psalm-type TAttributeValuesQuery = array{ 16 | * category_id?: int, 17 | * attribute_id?: int, 18 | * last_value_id?: int, 19 | * limit?: int, 20 | * language?: string 21 | * } 22 | */ 23 | class CategoryService extends AbstractService 24 | { 25 | private $path = '/v2/category'; 26 | 27 | /** 28 | * Receive the attributes list from the product page for a specified category. 29 | * 30 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_category_attribute 31 | * 32 | * @param TAttributeQuery $query 33 | * 34 | * @return mixed|\Psr\Http\Message\ResponseInterface 35 | */ 36 | public function attribute(int $categoryId, array $query = []): array 37 | { 38 | $query = ArrayHelper::pick($query, ['attribute_type', 'language']); 39 | $query = TypeCaster::castArr($query, [ 40 | 'attribute_type' => 'str', 41 | 'language' => 'str', 42 | ]); 43 | $query = array_merge([ 44 | 'category_id' => $categoryId, 45 | 'language' => 'RU', 46 | ], $query); 47 | 48 | return $this->request('POST', "{$this->path}/attribute", $query); 49 | } 50 | 51 | /** 52 | * Check the dictionary for attributes or options by theirs IDs. 53 | * 54 | * @param TAttributeValuesQuery $query 55 | * 56 | * @return array [result, has_next] 57 | */ 58 | public function attributeValues(int $categoryId, int $attrId, array $query = []): array 59 | { 60 | $query = ArrayHelper::pick($query, ['last_value_id', 'limit', 'language']); 61 | $query = array_merge([ 62 | 'category_id' => $categoryId, 63 | 'attribute_id' => $attrId, 64 | 'limit' => 1000, 65 | 'last_value_id' => 0, 66 | 'language' => 'RU', 67 | ], $query); 68 | $query = TypeCaster::castArr($query, [ 69 | 'category_id' => 'int', 70 | 'attribute_id' => 'int', 71 | 'last_value_id' => 'int', 72 | 'limit' => 'int', 73 | 'language' => 'str', 74 | ]); 75 | 76 | return $this->request('POST', "{$this->path}/attribute/values", $query, true, false); 77 | } 78 | 79 | public function attributeValueByOption(string $language = 'RU', array $options = []) 80 | { 81 | $options = $this->ensureCollection($options); 82 | foreach ($options as &$o) { 83 | $o = ArrayHelper::pick($o, ['attribute_id', 'option_id']); 84 | } 85 | unset($o); 86 | 87 | $body = [ 88 | 'language' => $language, 89 | 'options' => $options, 90 | ]; 91 | 92 | return $this->request('POST', "{$this->path}/attribute/value/by-option", $body); 93 | } 94 | 95 | /** 96 | * @see https://api-seller.ozon.ru/v2/category/tree 97 | */ 98 | public function tree(?int $categoryId = null, string $language = 'DEFAULT') 99 | { 100 | $body = [ 101 | 'language' => $language, 102 | ]; 103 | 104 | if ($categoryId) { 105 | assert($categoryId > 0); 106 | $body['category_id'] = $categoryId; 107 | } 108 | 109 | return $this->request('POST', "{$this->path}/tree", $body); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Service/V2/Posting/CrossborderService.php: -------------------------------------------------------------------------------- 1 | [], 26 | 'dir' => SortDirection::ASC, 27 | 'offset' => 0, 28 | 'limit' => 10, 29 | ]; 30 | 31 | $requestData = array_merge( 32 | $default, 33 | ArrayHelper::pick($requestData, array_keys($default)) 34 | ); 35 | 36 | $filter = ArrayHelper::pick($requestData['filter'], ['since', 'to', 'status']); 37 | foreach (['since', 'to'] as $key) { 38 | if (isset($filter[$key]) && $filter[$key] instanceof \DateTimeInterface) { 39 | $filter[$key] = $filter[$key]->format(DATE_RFC3339); 40 | } 41 | } 42 | $requestData['filter'] = $filter; 43 | 44 | return $this->request('POST', "{$this->path}/list", $requestData); 45 | } 46 | 47 | /** 48 | * @see https://cb-api.ozonru.me/apiref/en/#t-cb_unfulfilled_list 49 | * 50 | * @todo fix {"error":{"code":"BAD_REQUEST","message":"Invalid request payload","data":[{"name":"status","code":"TOO_FEW_ELEMENTS","value":"[]","message":""}]}} 51 | */ 52 | public function unfulfilledList(array $requestData = []): array 53 | { 54 | $default = [ 55 | 'status' => Status::getList(), 56 | 'dir' => SortDirection::ASC, 57 | 'offset' => 0, 58 | 'limit' => 10, 59 | ]; 60 | 61 | $requestData = array_merge( 62 | $default, 63 | ArrayHelper::pick($requestData, array_keys($default)) 64 | ); 65 | 66 | if (is_string($requestData['status'])) { 67 | $requestData['status'] = [$requestData['status']]; 68 | } 69 | 70 | return $this->request('POST', "{$this->path}/unfulfilled/list", $requestData); 71 | } 72 | 73 | /** 74 | * @see https://cb-api.ozonru.me/apiref/en/#t-cb_get 75 | */ 76 | public function get(string $postingNumber, array $options = []): array 77 | { 78 | return $this->request('POST', "{$this->path}/get", ['posting_number' => $postingNumber]); 79 | } 80 | 81 | /** 82 | * @see https://cb-api.ozonru.me/apiref/en/#t-cb_approve 83 | */ 84 | public function approve(string $postingNumber): bool 85 | { 86 | return $this->request('POST', "{$this->path}/approve", ['posting_number' => $postingNumber]); 87 | } 88 | 89 | /** 90 | * @see https://cb-api.ozonru.me/apiref/en/#t-cb_cancel 91 | * 92 | * @param array|string $sku 93 | */ 94 | public function cancel(string $postingNumber, $sku, int $cancelReasonId, string $cancelReasonMessage = ''): bool 95 | { 96 | if (is_string($sku)) { 97 | $sku = [$sku]; 98 | } 99 | $body = [ 100 | 'posting_number' => $postingNumber, 101 | 'sku' => $sku, 102 | 'cancel_reason_id' => $cancelReasonId, 103 | 'cancel_reason_message' => $cancelReasonMessage, 104 | ]; 105 | 106 | return $this->request('POST', "{$this->path}/cancel", $body); 107 | } 108 | 109 | public function cancelReasons(): array 110 | { 111 | return $this->request('POST', "{$this->path}/cancel-reason/list", '{}'); 112 | } 113 | 114 | /** 115 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_ship 116 | * 117 | * @return array list of postings IDs 118 | */ 119 | public function ship(string $postingNumber, string $track, int $shippingProviderId, array $items): array 120 | { 121 | foreach ($items as &$item) { 122 | $item = ArrayHelper::pick($item, ['quantity', 'sku']); 123 | } 124 | 125 | $body = [ 126 | 'posting_number' => $postingNumber, 127 | 'tracking_number' => $track, 128 | 'shipping_provider_id' => $shippingProviderId, 129 | 'items' => $items, 130 | ]; 131 | 132 | return $this->request('POST', "{$this->path}/ship", $body); 133 | } 134 | 135 | /** 136 | * @see https://cb-api.ozonru.me/apiref/en/#t-cb_shipping_provider_list 137 | */ 138 | public function shippingProviders(): array 139 | { 140 | return $this->request('POST', "{$this->path}/shipping-provider/list", '{}'); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Service/V2/Posting/FboService.php: -------------------------------------------------------------------------------- 1 | $requestData 37 | */ 38 | public function list(array $requestData = []): array 39 | { 40 | $default = [ 41 | 'filter' => [], 42 | 'dir' => SortDirection::ASC, 43 | 'offset' => 0, 44 | 'limit' => 10, 45 | 'with' => WithResolver::getDefaults(2, PostingScheme::FBO), 46 | ]; 47 | 48 | $requestData = array_merge( 49 | $default, 50 | ArrayHelper::pick($requestData, array_keys($default)) 51 | ); 52 | 53 | $filter = ArrayHelper::pick($requestData['filter'], ['since', 'to', 'status']); 54 | foreach (['since', 'to'] as $key) { 55 | if (isset($filter[$key]) && $filter[$key] instanceof \DateTimeInterface) { 56 | $filter[$key] = $filter[$key]->format(DATE_RFC3339); 57 | } 58 | } 59 | $requestData['filter'] = $filter; 60 | 61 | return $this->request('POST', "{$this->path}/list", $requestData); 62 | } 63 | 64 | /** 65 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbo_get 66 | */ 67 | public function get(string $postingNumber, array $options = []): array 68 | { 69 | return $this->request('POST', "{$this->path}/get", [ 70 | 'posting_number' => $postingNumber, 71 | 'with' => WithResolver::resolve($options, 2, PostingScheme::FBO), 72 | ]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Service/V2/Posting/FbsService.php: -------------------------------------------------------------------------------- 1 | $requestData 45 | * 46 | * @deprecated use V3\Posting\FbsService::list 47 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_list 48 | */ 49 | public function list(array $requestData = []): array 50 | { 51 | $default = [ 52 | 'filter' => [], 53 | 'dir' => SortDirection::ASC, 54 | 'offset' => 0, 55 | 'limit' => 10, 56 | ]; 57 | 58 | $requestData = array_merge( 59 | $default, 60 | ArrayHelper::pick($requestData, array_keys($default)) 61 | ); 62 | 63 | $filter = ArrayHelper::pick($requestData['filter'], ['since', 'to', 'status']); 64 | foreach (['since', 'to'] as $key) { 65 | if (isset($filter[$key]) && $filter[$key] instanceof \DateTimeInterface) { 66 | $filter[$key] = $filter[$key]->format(DATE_RFC3339); 67 | } 68 | } 69 | $requestData['filter'] = $filter; 70 | 71 | return $this->request('POST', "{$this->path}/list", $requestData); 72 | } 73 | 74 | /** 75 | * @deprecated use V3\Posting\FbsService::unfulfilledList 76 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_unfulfilled_list 77 | */ 78 | public function unfulfilledList(array $requestData = []): array 79 | { 80 | $default = [ 81 | 'with' => WithResolver::resolve($requestData, 2, PostingScheme::FBS, __FUNCTION__), 82 | 'status' => Status::getList(), 83 | 'sort_by' => 'updated_at', 84 | 'dir' => SortDirection::ASC, 85 | 'offset' => 0, 86 | 'limit' => 10, 87 | ]; 88 | 89 | $requestData = array_merge( 90 | $default, 91 | ArrayHelper::pick($requestData, array_keys($default)) 92 | ); 93 | 94 | if (is_string($requestData['status'])) { 95 | $requestData['status'] = [$requestData['status']]; 96 | } 97 | 98 | return $this->request('POST', "{$this->path}/unfulfilled/list", $requestData); 99 | } 100 | 101 | /** 102 | * @deprecated use V3\Posting\FbsService::get 103 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_get 104 | */ 105 | public function get(string $postingNumber, array $options = []): array 106 | { 107 | return $this->request('POST', "{$this->path}/get", [ 108 | 'posting_number' => $postingNumber, 109 | 'with' => WithResolver::resolve($options, 2, PostingScheme::FBS), 110 | ]); 111 | } 112 | 113 | /** 114 | * @return array list of postings IDs 115 | * 116 | * @deprecated use V3\Posting\FbsService::ship 117 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_ship 118 | */ 119 | public function ship(array $packages, string $postingNumber): array 120 | { 121 | foreach ($packages as &$package) { 122 | $package = ArrayHelper::pick($package, ['items']); 123 | } 124 | 125 | $body = [ 126 | 'packages' => $packages, 127 | 'posting_number' => $postingNumber, 128 | ]; 129 | 130 | return $this->request('POST', "{$this->path}/ship", $body); 131 | } 132 | 133 | /** 134 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_package_label 135 | * 136 | * @param array|string $postingNumber 137 | */ 138 | public function packageLabel($postingNumber): string 139 | { 140 | if (is_string($postingNumber)) { 141 | $postingNumber = [$postingNumber]; 142 | } 143 | 144 | return $this->request('POST', "{$this->path}/package-label", ['posting_number' => $postingNumber], false); 145 | } 146 | 147 | /** 148 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_CreateLabelBatchV2 149 | * 150 | * @param array|string $postingNumber 151 | */ 152 | public function packageLabelCreate($postingNumber): array 153 | { 154 | if (is_string($postingNumber)) { 155 | $postingNumber = [$postingNumber]; 156 | } 157 | 158 | return $this->request('POST', "{$this->path}/package-label/create", ['posting_number' => $postingNumber]); 159 | } 160 | 161 | /** 162 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_arbitration_title 163 | * 164 | * @param array|string $postingNumber 165 | */ 166 | public function arbitration($postingNumber): bool 167 | { 168 | if (is_string($postingNumber)) { 169 | $postingNumber = [$postingNumber]; 170 | } 171 | 172 | $result = $this->request('POST', "{$this->path}/arbitration", ['posting_number' => $postingNumber]); 173 | 174 | return 'true' === $result; 175 | } 176 | 177 | /** 178 | * @see https://cb-api.ozonru.me/apiref/en/#t-fbs_cancel_title 179 | */ 180 | public function cancel(string $postingNumber, int $cancelReasonId, ?string $cancelReasonMessage = null): bool 181 | { 182 | $body = [ 183 | 'posting_number' => $postingNumber, 184 | 'cancel_reason_id' => $cancelReasonId, 185 | 'cancel_reason_message' => $cancelReasonMessage, 186 | ]; 187 | $result = $this->request('POST', "{$this->path}/cancel", $body); 188 | 189 | return 'true' === $result; 190 | } 191 | 192 | public function cancelReasons(): array 193 | { 194 | return $this->request('POST', "{$this->path}/cancel-reason/list", '{}'); // todo свериться с исправленной документацией 195 | } 196 | 197 | /** 198 | * @param string|array $postingNumber 199 | * 200 | * @return array|string 201 | * 202 | * @todo return true 203 | */ 204 | public function awaitingDelivery($postingNumber) 205 | { 206 | if (is_string($postingNumber)) { 207 | $postingNumber = [$postingNumber]; 208 | } 209 | 210 | $body = [ 211 | 'posting_number' => $postingNumber, 212 | ]; 213 | 214 | return $this->request('POST', "{$this->path}/awaiting-delivery", $body); 215 | } 216 | 217 | public function getByBarcode(string $barcode): array 218 | { 219 | return $this->request('POST', "{$this->path}/get-by-barcode", ['barcode' => $barcode]); 220 | } 221 | 222 | // 223 | 224 | /** 225 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_PostingFBSActCreate 226 | * 227 | * @param array $params [containers_count, delivery_method_id] 228 | */ 229 | public function actCreate(array $params): int 230 | { 231 | $config = [ 232 | 'containers_count' => 'int', 233 | 'delivery_method_id' => 'int', 234 | ]; 235 | 236 | $params = ArrayHelper::pick($params, array_keys($config)); 237 | $params = TypeCaster::castArr($params, $config); 238 | $result = $this->request('POST', "{$this->path}/act/create", $params); 239 | 240 | return $result['id']; 241 | } 242 | 243 | /** 244 | * @see https://cb-api.ozonru.me/apiref/en/#t-section_postings_fbs_act_check_title 245 | */ 246 | public function actCheckStatus(int $id): array 247 | { 248 | return $this->request('POST', "{$this->path}/act/check-status", ['id' => $id]); 249 | } 250 | 251 | /** 252 | * @see https://cb-api.ozonru.me/apiref/en/#t-section_postings_fbs_act_get_title 253 | */ 254 | public function actGetPdf(int $id): string 255 | { 256 | return $this->request('POST', "{$this->path}/act/get-pdf", ['id' => $id], false); 257 | } 258 | 259 | public function actGetContainerLabels(int $id): string 260 | { 261 | return $this->request('POST', "{$this->path}/act/get-container-labels", ['id' => $id], false); 262 | } 263 | 264 | // 265 | 266 | // 267 | 268 | /** 269 | * @param array|string $postingNumber 270 | */ 271 | public function delivered($postingNumber): array 272 | { 273 | if (is_string($postingNumber)) { 274 | $postingNumber = [$postingNumber]; 275 | } 276 | 277 | $body = [ 278 | 'posting_number' => $postingNumber, 279 | ]; 280 | 281 | return $this->request('POST', '/v2/fbs/posting/delivered', $body); 282 | } 283 | 284 | /** 285 | * @param array|string $postingNumber 286 | */ 287 | public function delivering($postingNumber): array 288 | { 289 | if (is_string($postingNumber)) { 290 | $postingNumber = [$postingNumber]; 291 | } 292 | 293 | $body = [ 294 | 'posting_number' => $postingNumber, 295 | ]; 296 | 297 | return $this->request('POST', '/v2/fbs/posting/delivering', $body); 298 | } 299 | 300 | /** 301 | * @param array|string $postingNumber 302 | */ 303 | public function lastMile($postingNumber): array 304 | { 305 | if (is_string($postingNumber)) { 306 | $postingNumber = [$postingNumber]; 307 | } 308 | 309 | $body = [ 310 | 'posting_number' => $postingNumber, 311 | ]; 312 | 313 | return $this->request('POST', '/v2/fbs/posting/last-mile', $body); 314 | } 315 | 316 | public function setTrackingNumber(array $trackingNumbers): array 317 | { 318 | if (isset($trackingNumbers['posting_number']) || isset($trackingNumbers['tracking_number'])) { 319 | $trackingNumbers = [$trackingNumbers]; 320 | } 321 | 322 | foreach ($trackingNumbers as &$tn) { 323 | $tn = ArrayHelper::pick($tn, ['posting_number', 'tracking_number']); 324 | } 325 | 326 | $body = [ 327 | 'tracking_numbers' => $trackingNumbers, 328 | ]; 329 | 330 | return $this->request('POST', '/v2/fbs/posting/tracking-number/set', $body); 331 | } 332 | 333 | // 334 | 335 | /** 336 | * @param 'act_of_acceptance'|'act_of_mismatch'|'act_of_excess' $docType 337 | * 338 | * @return array{header: array, rows: array} 339 | */ 340 | public function digitalActGetPdf(int $id, string $docType): array 341 | { 342 | return $this->request('POST', "{$this->path}/digital/act/get-pdf", [ 343 | 'id' => $id, 344 | 'doc_type' => $docType, 345 | ]); 346 | } 347 | 348 | /** 349 | * Receive list of manufacturing countries. 350 | * 351 | * @see https://docs.ozon.ru/api/seller/en/#operation/PostingAPI_ChangeFbsPostingProduct 352 | * 353 | * @return list 354 | */ 355 | public function productCountryList(string $nameSearch): array 356 | { 357 | $body = [ 358 | 'name_search' => $nameSearch, 359 | ]; 360 | 361 | return $this->request('POST', "{$this->path}/product/country/list", $body); 362 | } 363 | 364 | /** 365 | * Set the manufacturing country. 366 | * 367 | * @see https://docs.ozon.ru/api/seller/en/#operation/PostingAPI_SetCountryProductFbsPostingV2 368 | * 369 | * @return TProductCountrySetResponse 370 | */ 371 | public function productCountrySet(string $postingNumber, int $productId, string $countryIsoCode): array 372 | { 373 | $body = [ 374 | 'posting_number' => $postingNumber, 375 | 'product_id' => $productId, 376 | 'country_iso_code' => $countryIsoCode, 377 | ]; 378 | 379 | return $this->request('POST', "{$this->path}/product/country/set", $body); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/Service/V2/ProductService.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @psalm-type TInfoQuery = array{ 16 | * product_id?: int, 17 | * sku?: int, 18 | * offer_id?: string 19 | * } 20 | */ 21 | class ProductService extends AbstractService 22 | { 23 | private $path = '/v2/product'; 24 | 25 | /** 26 | * Creates product page in our system. 27 | * 28 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_product_import 29 | * 30 | * @param array $income Single item structure or array of items 31 | * 32 | * @return array 33 | */ 34 | public function import(array $income, bool $validateBeforeSend = true) 35 | { 36 | if (!array_key_exists('items', $income)) { 37 | $income = $this->ensureCollection($income); 38 | $income = ['items' => $income]; 39 | } 40 | 41 | $income = ArrayHelper::pick($income, ['items']); 42 | 43 | if ($validateBeforeSend) { 44 | $pv = new ProductValidator('create', 2); 45 | foreach ($income['items'] as &$item) { 46 | $item = $pv->validateItem($item); 47 | } 48 | } 49 | 50 | return $this->request('POST', "{$this->path}/import", $income); 51 | } 52 | 53 | /** 54 | * @deprecated use V3\ProductService::list 55 | * 56 | * Receive the list of products. 57 | * 58 | * query['filter'] 59 | * [offer_id] string|int|array 60 | * [product_id] string|int|array, 61 | * [visibility] string 62 | * [last_id] str 63 | * [limit] int 64 | * @see http://cb-api.ozonru.me/apiref/en/#t-title_get_products_list 65 | */ 66 | public function list(array $query) 67 | { 68 | $query = ArrayHelper::pick($query, ['filter', 'last_id', 'limit']); 69 | $query = TypeCaster::castArr($query, ['last_id' => 'str', 'limit' => 'int']); 70 | if (isset($query['filter'])) { 71 | $query['filter'] = TypeCaster::castArr( 72 | ArrayHelper::pick($query['filter'], ['offer_id', 'product_id', 'visibility']), 73 | ['offer_id' => 'arrOfStr', 'product_id' => 'arrOfInt', 'visibility' => 'str'] 74 | ); 75 | } 76 | 77 | return $this->request('POST', "{$this->path}/list", $query); 78 | } 79 | 80 | /** 81 | * @deprecated use V3\ProductService::list 82 | * 83 | * Receive product info 84 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_products_info 85 | * 86 | * @param array $query ['product_id', 'sku', 'offer_id'] 87 | */ 88 | public function info(array $query): array 89 | { 90 | $query = ArrayHelper::pick($query, ['product_id', 'sku', 'offer_id']); 91 | $query = TypeCaster::castArr($query, ['product_id' => 'int', 'sku' => 'int', 'offer_id' => 'str']); 92 | 93 | return $this->request('POST', "{$this->path}/info", $query); 94 | } 95 | 96 | /** 97 | * @deprecated use V3\ProductService::infoList 98 | */ 99 | public function infoList(array $query): array 100 | { 101 | $query = ArrayHelper::pick($query, ['product_id', 'sku', 'offer_id']); 102 | $query = TypeCaster::castArr($query, [ 103 | 'product_id' => 'arrOfInt', 104 | 'sku' => 'arrOfInt', 105 | 'offer_id' => 'arrOfStr', 106 | ]); 107 | 108 | return $this->request('POST', "{$this->path}/info/list", $query); 109 | } 110 | 111 | /** 112 | * @deprecated use V3\ProductService::infoAttributes 113 | * @see https://cb-api.ozonru.me/apiref/en/#t-title_products_info_attributes 114 | */ 115 | public function infoAttributes(array $filter, int $page = 1, int $pageSize = 100): array 116 | { 117 | $keys = ['offer_id', 'product_id']; 118 | $filter = ArrayHelper::pick($filter, $keys); 119 | 120 | foreach ($keys as $k) { 121 | if (isset($filter[$k]) && !is_array($filter[$k])) { 122 | $filter[$k] = [$filter[$k]]; 123 | } 124 | } 125 | 126 | if (isset($filter['offer_id'])) { 127 | $filter['offer_id'] = array_map('strval', $filter['offer_id']); 128 | } 129 | 130 | $query = [ 131 | 'filter' => $filter, 132 | 'page' => $page, 133 | 'page_size' => $pageSize, 134 | ]; 135 | 136 | return $this->request('POST', "{$this->path}s/info/attributes", $query); 137 | } 138 | 139 | /** 140 | * @param array $pagination ['page', 'page_size'] 141 | * 142 | * @return array {items: array, total: int} 143 | * 144 | * Receive products stocks info 145 | * 146 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_GetProductInfoPricesV2 147 | */ 148 | public function infoStocks(array $pagination = []): array 149 | { 150 | $pagination = array_merge( 151 | ['page' => 1, 'page_size' => 100], 152 | ArrayHelper::pick($pagination, ['page', 'page_size']) 153 | ); 154 | 155 | return $this->request('POST', "{$this->path}/info/stocks", $pagination); 156 | } 157 | 158 | /** 159 | * @param array $pagination [page, page_size] 160 | * 161 | * @return array 162 | * 163 | * @deprecated use V4\ProductService::infoPrices 164 | * 165 | * Receive products prices info 166 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_GetProductInfoListV2 167 | */ 168 | public function infoPrices(array $pagination = []) 169 | { 170 | $pagination = array_merge( 171 | ['page' => 1, 'page_size' => 100], 172 | ArrayHelper::pick($pagination, ['page', 'page_size']) 173 | ); 174 | 175 | return $this->request('POST', "{$this->path}/info/prices", $pagination); 176 | } 177 | 178 | /** 179 | * Update product stocks. 180 | * 181 | * @return array 182 | * 183 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_ProductsStocksV2 184 | */ 185 | public function importStocks(array $input) 186 | { 187 | if (empty($input)) { 188 | throw new \InvalidArgumentException('Empty stocks data'); 189 | } 190 | 191 | if ($this->isAssoc($input) && !isset($input['stocks'])) {// if it one price 192 | $input = ['stocks' => [$input]]; 193 | } else { 194 | if (!$this->isAssoc($input)) {// if it plain array on prices 195 | $input = ['stocks' => $input]; 196 | } 197 | } 198 | 199 | if (!isset($input['stocks'])) { 200 | throw new \InvalidArgumentException(); 201 | } 202 | 203 | foreach ($input['stocks'] as $i => &$s) { 204 | if (!$s = ArrayHelper::pick($s, ['product_id', 'offer_id', 'stock', 'warehouse_id'])) { 205 | throw new \InvalidArgumentException('Invalid stock data at index '.$i); 206 | } 207 | 208 | $s = TypeCaster::castArr( 209 | $s, 210 | [ 211 | 'product_id' => 'int', 212 | 'offer_id' => 'str', 213 | 'stock' => 'int', 214 | 'warehouse_id' => 'int', 215 | ] 216 | ); 217 | } 218 | 219 | return $this->request('POST', "{$this->path}s/stocks", $input); 220 | } 221 | 222 | /** 223 | * @param array $input one of:
224 | * {products:[{offer_id: "str"}, ...]}
225 | * [{offer_id: "str"}, ...]
226 | * {offer_id: "str"}
227 | * 228 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_DeleteProducts 229 | */ 230 | public function delete(array $input) 231 | { 232 | if ($this->isAssoc($input) && !isset($input['products'])) {// if it one price 233 | $input = ['products' => [$input]]; 234 | } else { 235 | if (!$this->isAssoc($input)) {// if it plain array on prices 236 | $input = ['products' => $input]; 237 | } 238 | } 239 | 240 | if (!isset($input['products'])) { 241 | throw new \InvalidArgumentException(); 242 | } 243 | 244 | foreach ($input['products'] as $i => &$s) { 245 | if (!$s = ArrayHelper::pick($s, ['offer_id'])) { 246 | throw new \InvalidArgumentException('Invalid stock data at index '.$i); 247 | } 248 | 249 | $s = TypeCaster::castArr( 250 | $s, 251 | [ 252 | 'offer_id' => 'str', 253 | ] 254 | ); 255 | } 256 | 257 | return $this->request('POST', "{$this->path}s/delete", $input); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Service/V2/ReturnsService.php: -------------------------------------------------------------------------------- 1 | array, 'offset' => int, 'limit' => int] 18 | */ 19 | public function company(string $postingScheme, array $requestData): array 20 | { 21 | $postingScheme = strtolower($postingScheme); 22 | if (!in_array($postingScheme, [PostingScheme::FBO, PostingScheme::FBS])) { 23 | throw new \LogicException("Unsupported posting scheme: $postingScheme"); 24 | } 25 | 26 | $default = [ 27 | 'filter' => [], 28 | 'offset' => 0, 29 | 'limit' => 10, 30 | ]; 31 | 32 | $requestData = array_merge( 33 | $default, 34 | ArrayHelper::pick($requestData, array_keys($default)) 35 | ); 36 | 37 | return $this->request('POST', "{$this->path}/company/{$postingScheme}", $requestData); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Service/V3/CategoryService.php: -------------------------------------------------------------------------------- 1 | 'str', 27 | 'language' => 'str', 28 | ]); 29 | $query = array_merge([ 30 | 'category_id' => (array) $categoryId, 31 | 'language' => Language::DEFAULT, 32 | ], $query); 33 | 34 | return $this->request('POST', "{$this->path}/attribute", $query); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Service/V3/Posting/FbsService.php: -------------------------------------------------------------------------------- 1 | WithResolver::getDefaults(3, PostingScheme::FBS), 27 | 'filter' => [], 28 | 'dir' => SortDirection::ASC, 29 | 'offset' => 0, 30 | 'limit' => 10, 31 | ]; 32 | 33 | $requestData = array_merge( 34 | $default, 35 | ArrayHelper::pick($requestData, array_keys($default)) 36 | ); 37 | 38 | $requestData['filter'] = ArrayHelper::pick($requestData['filter'], [ 39 | 'delivery_method_id', 40 | 'order_id', 41 | 'provider_id', 42 | 'status', 43 | 'since', 44 | 'to', 45 | 'warehouse_id', 46 | ]); 47 | 48 | // default filter parameters 49 | $requestData['filter'] = array_merge( 50 | [ 51 | 'since' => (new \DateTime('now - 7 days'))->format(DATE_W3C), 52 | 'to' => (new \DateTime('now'))->format(DATE_W3C), 53 | ], 54 | $requestData['filter'] 55 | ); 56 | 57 | return $this->request('POST', "{$this->path}/list", $requestData); 58 | } 59 | 60 | public function unfulfilledList(array $requestData = []): array 61 | { 62 | $default = [ 63 | 'with' => WithResolver::getDefaults(3, PostingScheme::FBS), 64 | 'filter' => [], 65 | 'dir' => SortDirection::ASC, 66 | 'offset' => 0, 67 | 'limit' => 10, 68 | ]; 69 | 70 | $requestData = array_merge( 71 | $default, 72 | ArrayHelper::pick($requestData, array_keys($default)) 73 | ); 74 | 75 | $requestData['filter'] = ArrayHelper::pick($requestData['filter'], [ 76 | 'cutoff_from', 77 | 'cutoff_to', 78 | 'delivering_date_from', 79 | 'delivering_date_to', 80 | 'delivery_method_id', 81 | 'provider_id', 82 | 'status', 83 | 'warehouse_id', 84 | ]); 85 | 86 | // https://github.com/gam6itko/ozon-seller/issues/48 87 | if ( 88 | (empty($requestData['filter']['cutoff_from']) && empty($requestData['filter']['cutoff_to'])) 89 | && (empty($requestData['filter']['delivering_date_from']) && empty($requestData['filter']['delivering_date_to'])) 90 | ) { 91 | throw new \LogicException('Not defined mandatory filter date ranges `cutoff` or `delivering_date`'); 92 | } 93 | 94 | return $this->request('POST', "{$this->path}/unfulfilled/list", $requestData); 95 | } 96 | 97 | public function get(string $postingNumber, array $options = []): array 98 | { 99 | return $this->request('POST', "{$this->path}/get", [ 100 | 'posting_number' => $postingNumber, 101 | 'with' => WithResolver::resolve($options, 3, PostingScheme::FBS), 102 | ]); 103 | } 104 | 105 | /** 106 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_ShipFbsPostingV3 107 | * 108 | * @return array list of postings IDs 109 | */ 110 | public function ship(array $packages, string $postingNumber, array $options = []): array 111 | { 112 | foreach ($packages as &$package) { 113 | $package = ArrayHelper::pick($package, ['products']); 114 | } 115 | 116 | $body = [ 117 | 'packages' => $packages, 118 | 'posting_number' => $postingNumber, 119 | 'with' => WithResolver::resolve($options, 3, PostingScheme::FBS, __FUNCTION__), 120 | ]; 121 | 122 | return $this->request('POST', "$this->path/ship", $body); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Service/V3/ProductService.php: -------------------------------------------------------------------------------- 1 | , 15 | * product_id: list, 16 | * visibility: string 17 | * } 18 | * @psalm-type TInfoStocksResponseStocks = array{ 19 | * present: int, 20 | * reserver: int, 21 | * type: string 22 | * } 23 | * @psalm-type TInfoStocksResponseItem = array{ 24 | * offer_id: string, 25 | * product_id: int, 26 | * stocks: list 27 | * } 28 | * @psalm-type TInfoStocksResponse = array{ 29 | * items: list, 30 | * last_id: string, 31 | * limit: int 32 | * } 33 | * @psalm-type TProductListRequestFilter = array{ 34 | * offer_id?: list, 35 | * product_id?: list, 36 | * visibility?: string 37 | * } 38 | * @psalm-type TProductListResponseItemQuant = array{ 39 | * quant_code: string, 40 | * quant_size: int, 41 | * } 42 | * @psalm-type TProductListResponseItem = array{ 43 | * archived: bool, 44 | * has_fbo_stocks: bool, 45 | * has_fbs_stocks: bool, 46 | * is_discounted: bool, 47 | * offer_id: string, 48 | * product_id: int, 49 | * quants: list 50 | * } 51 | * @psalm-type TProductListResponse = array{ 52 | * items: list, 53 | * last_id: string, 54 | * total: int 55 | * } 56 | * @psalm-type TProductInfoListResponse = array{ 57 | * items: list>, 58 | * } 59 | * @psalm-type TProductInfoListRequest = array{ 60 | * offer_id?: list, 61 | * product_id?: list, 62 | * sku?: list 63 | * } 64 | */ 65 | class ProductService extends AbstractService 66 | { 67 | private $path = '/v3/product'; 68 | 69 | public function importStocks(array $filter, ?string $lastId = '', int $limit = 100) 70 | { 71 | assert($limit > 0 && $limit <= 1000); 72 | 73 | $body = [ 74 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']), 75 | 'last_id' => $lastId ?? '', 76 | 'limit' => $limit, 77 | ]; 78 | 79 | return $this->request('POST', "{$this->path}s/stocks", $body); 80 | } 81 | 82 | public function infoAttributes(array $filter, ?string $lastId = '', int $limit = 100, string $sortBy = 'product_id', string $sortDir = SortDirection::DESC) 83 | { 84 | $body = [ 85 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']), 86 | 'last_id' => $lastId ?? '', 87 | 'limit' => $limit, 88 | 'sort_by' => $sortBy, 89 | 'sort_dir' => $sortDir, 90 | ]; 91 | 92 | return $this->request('POST', "{$this->path}s/info/attributes", $body); 93 | } 94 | 95 | /** 96 | * @deprecated use V4\ProductService::infoStocks 97 | * 98 | * @param TInfoStocksRequestFilter $filter 99 | * 100 | * @return TInfoStocksResponse 101 | */ 102 | public function infoStocks(array $filter, ?string $lastId = '', int $limit = 100): array 103 | { 104 | $body = [ 105 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']), 106 | 'last_id' => $lastId ?? '', 107 | 'limit' => $limit, 108 | ]; 109 | 110 | return $this->request('POST', "{$this->path}/info/stocks", $body); 111 | } 112 | 113 | /** 114 | * Method for getting a list of all products. 115 | * 116 | * @see https://docs.ozon.ru/api/seller/en/#operation/ProductAPI_GetProductListv3 117 | * 118 | * @param TProductListRequestFilter $filter 119 | * 120 | * @return TProductListResponse 121 | */ 122 | public function list(array $filter, string $lastId = '', int $limit = 100): array 123 | { 124 | $body = [ 125 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']) ?: new \stdClass(), 126 | 'last_id' => $lastId, 127 | 'limit' => $limit, 128 | ]; 129 | 130 | return $this->request('POST', "{$this->path}/list", $body); 131 | } 132 | 133 | /** 134 | * Method for getting an array of products by their identifiers. 135 | * 136 | * @see https://docs.ozon.ru/api/seller/en/#operation/ProductAPI_GetProductInfoList 137 | * 138 | * @param TProductInfoListRequest $query 139 | * 140 | * @return TProductInfoListResponse 141 | */ 142 | public function infoList(array $query): array 143 | { 144 | $query = ArrayHelper::pick($query, ['product_id', 'sku', 'offer_id']); 145 | $query = TypeCaster::castArr($query, [ 146 | 'product_id' => 'arrayOfString', 147 | 'sku' => 'arrayOfString', 148 | 'offer_id' => 'arrayOfString', 149 | ]); 150 | 151 | if (empty($query)) { 152 | throw new \InvalidArgumentException('Empty query provided'); 153 | } 154 | 155 | return $this->request('POST', "{$this->path}/info/list", $query); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Service/V3/ReturnService.php: -------------------------------------------------------------------------------- 1 | 0 && $limit <= 1000); 29 | 30 | $body = [ 31 | 'filter' => ArrayHelper::pick($filter, ['posting_number', 'status']), 32 | 'last_id' => $lastId, 33 | 'limit' => $limit, 34 | ]; 35 | 36 | return $this->request('POST', "{$this->path}/fbo", $body); 37 | } 38 | 39 | /** 40 | * @deprecated use V1\ReturnService::list 41 | * 42 | * @see https://api-seller.ozon.ru/v3/returns/company/fbs 43 | * 44 | * @param array $filter 45 | * @param int $lastId 46 | * @param int $limit 47 | * 48 | * @return array 49 | */ 50 | public function fbs(array $filter, int $lastId = 0, int $limit = 100): array 51 | { 52 | assert($limit > 0 && $limit <= 1000); 53 | 54 | $body = [ 55 | 'filter' => ArrayHelper::pick($filter, [ 56 | 'accepted_from_customer_moment', 'last_free_waiting_day', 'posting_number', 57 | 'product_name', 'product_offer_id', 'status' 58 | ]), 59 | 'last_id' => $lastId, 60 | 'limit' => $limit, 61 | ]; 62 | 63 | return $this->request('POST', "{$this->path}/fbs", $body); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Service/V4/Posting/FbsService.php: -------------------------------------------------------------------------------- 1 | , 15 | * name: string, 16 | * offer_id: string, 17 | * price: string, 18 | * quantity: integer, 19 | * sku: integer, 20 | * currency_code: string 21 | * } 22 | * @psalm-type TShipResponseAdditionalData = array{ 23 | * posting_number: string, 24 | * products: list 25 | * } 26 | * @psalm-type TShipResponse = array{ 27 | * additional_data: TShipResponseAdditionalData, 28 | * result: list 29 | * } 30 | * @psalm-type THasProducts = array{ 31 | * products: list 32 | * } 33 | * @psalm-type TShipProduct = array{ 34 | * product_id: int, 35 | * quantity: int 36 | * } 37 | * @psalm-type TShipPackageProduct = array{ 38 | * exemplarsIds: int, 39 | * product_id: int 40 | * quantity: int 41 | * } 42 | * 43 | * @psalm-type TShipWith = array{additional_data: bool} 44 | */ 45 | class FbsService extends AbstractService 46 | { 47 | private $path = '/v4/posting/fbs'; 48 | 49 | /** 50 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_ShipFbsPostingV4 51 | * 52 | * @param $packages list 53 | * @param $with TShipWith 54 | * 55 | * @return TShipResponse 56 | */ 57 | public function ship(array $packages, string $postingNumber, array $with = []): array 58 | { 59 | \assert([] !== $packages); 60 | \assert(!$this->isAssoc($packages)); 61 | foreach ($packages as &$package) { 62 | \assert(\array_key_exists('products', $package)); 63 | $package = ArrayHelper::pick($package, ['products']); 64 | \assert(!$this->isAssoc($package['products'])); 65 | \assert(\count($package['products']) > 0); 66 | } 67 | 68 | $body = [ 69 | 'packages' => $packages, 70 | 'posting_number' => $postingNumber, 71 | 'with' => WithResolver::resolve($with, 4, PostingScheme::FBS, __FUNCTION__), 72 | ]; 73 | 74 | return $this->request('POST', "$this->path/ship", $body); 75 | } 76 | 77 | /** 78 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_ShipFbsPostingPackage 79 | * 80 | * @return string PostingNumber 81 | */ 82 | public function shipPackage(string $postingNumber, array $products): string 83 | { 84 | $body = [ 85 | 'posting_number' => $postingNumber, 86 | 'products' => $products, 87 | ]; 88 | 89 | return $this->request('POST', "$this->path/ship/package", $body); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Service/V4/ProductService.php: -------------------------------------------------------------------------------- 1 | , 17 | * product_id?: list, 18 | * visibility?: string, 19 | * with_quant?: TInfoStocksQuantRequestFilter 20 | * } 21 | * @psalm-type TInfoStocksResponseStocks = array{ 22 | * present: int, 23 | * reserved: int, 24 | * shipment_type: string, 25 | * sku: int, 26 | * type: string 27 | * } 28 | * @psalm-type TInfoStocksResponseItem = array{ 29 | * offer_id: string, 30 | * product_id: int, 31 | * stocks: list 32 | * } 33 | * @psalm-type TInfoStocksResponse = array{ 34 | * items: list, 35 | * cursor: string, 36 | * total: int 37 | * } 38 | */ 39 | class ProductService extends AbstractService 40 | { 41 | private $path = '/v4/product'; 42 | 43 | /** 44 | * @deprecated use V5\ProductService::infoPrices 45 | * @see https://api-seller.ozon.ru/v4/product/info/prices 46 | */ 47 | public function infoPrices(array $filter, ?string $lastId = '', int $limit = 100) 48 | { 49 | assert($limit > 0 && $limit <= 1000); 50 | 51 | $body = [ 52 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']) ?: new \stdClass(), 53 | 'last_id' => $lastId ?? '', 54 | 'limit' => $limit, 55 | ]; 56 | 57 | return $this->request('POST', "{$this->path}/info/prices", $body); 58 | } 59 | 60 | /** 61 | * @see https://docs.ozon.ru/api/seller/#operation/ProductAPI_GetUploadQuota 62 | * 63 | * @psalm-type TQuota = array{ 64 | * limit: int, 65 | * reset_at: string, 66 | * usage: int 67 | * } 68 | * 69 | * @return array{ 70 | * daily_create: TQuota, 71 | * daily_update: TQuota, 72 | * total: array{ 73 | * limit: int, 74 | * usage: int, 75 | * }, 76 | * } 77 | */ 78 | public function infoLimit(): array 79 | { 80 | return $this->request('POST', "{$this->path}/info/limit", '{}'); 81 | } 82 | 83 | /** 84 | * Returns information about the quantity of products: 85 | * - how many items are available, 86 | * - how many are reserved by customers. 87 | * 88 | * @see https://docs.ozon.ru/api/seller/en/#operation/ProductAPI_GetProductInfoStocks 89 | * 90 | * @param TInfoStocksRequestFilter $filter 91 | * 92 | * @return TInfoStocksResponse 93 | */ 94 | public function infoStocks(array $filter, string $cursor = '', int $limit = 100): array 95 | { 96 | $body = [ 97 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility', 'with_quant']) ?: new \stdClass(), 98 | 'cursor' => $cursor, 99 | 'limit' => $limit, 100 | ]; 101 | 102 | return $this->request('POST', "{$this->path}/info/stocks", $body); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Service/V5/Posting/FbsService.php: -------------------------------------------------------------------------------- 1 | $postingNumber, 21 | ]; 22 | return $this->request('POST', "{$this->path}/product/exemplar/create-or-get", $body); 23 | } 24 | 25 | /** 26 | * @see https://docs.ozon.ru/api/seller/#operation/PostingAPI_FbsPostingProductExemplarSet 27 | */ 28 | public function productExemplarSet(int $multiBoxQty, string $postingNumber, array $products): bool 29 | { 30 | $body = [ 31 | 'multi_box_qty' => $multiBoxQty, 32 | 'posting_number' => $postingNumber, 33 | 'products' => $products, 34 | ]; 35 | return $this->request('POST', "{$this->path}/product/exemplar/set", $body); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/V5/ProductService.php: -------------------------------------------------------------------------------- 1 | , 13 | * product_id?: list, 14 | * visibility?: string, 15 | * } 16 | * @psalm-type TInfoPricesResponse = array{ 17 | * items: list>, 18 | * cursor: string, 19 | * total: int 20 | * } 21 | */ 22 | class ProductService extends AbstractService 23 | { 24 | private $path = '/v5/product'; 25 | 26 | /** 27 | * @see https://docs.ozon.ru/api/seller/en/#operation/ProductAPI_GetProductInfoPrices 28 | * 29 | * @param TInfoPricesRequestFilter $filter 30 | * 31 | * @return TInfoPricesResponse 32 | */ 33 | public function infoPrices(array $filter, string $cursor = '', int $limit = 100) 34 | { 35 | assert($limit > 0 && $limit <= 1000); 36 | 37 | $body = [ 38 | 'filter' => ArrayHelper::pick($filter, ['offer_id', 'product_id', 'visibility']) ?: new \stdClass(), 39 | 'cursor' => $cursor, 40 | 'limit' => $limit, 41 | ]; 42 | 43 | return $this->request('POST', "{$this->path}/info/prices", $body); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/TypeCaster.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class TypeCaster 11 | { 12 | /** 13 | * @param array $data Array with cast types 14 | * @param array $config ['float_key' => 'float', 'str_key' => 'string', 'int_key' => 'int'] 15 | * 16 | * @return array Modified data 17 | */ 18 | public static function castArr(array $data, array $config) 19 | { 20 | foreach ($data as $key => &$val) { 21 | if (array_key_exists($key, $config) && null !== $val) { 22 | $val = self::cast($val, $config[$key]); 23 | } 24 | } 25 | 26 | return $data; 27 | } 28 | 29 | public static function cast($val, string $type) 30 | { 31 | switch (self::normalizeType($type)) { 32 | case 'boolean': 33 | return (bool) $val; 34 | case 'string': 35 | return (string) $val; 36 | case 'integer': 37 | return (int) $val; 38 | case 'float': 39 | return (float) $val; 40 | case 'array': 41 | return (array) $val; 42 | case 'arrayOfInt': 43 | return array_map(function ($v): int { 44 | return (int) $v; 45 | }, (array) $val); 46 | case 'arrayOfString': 47 | return array_map(function ($v): string { 48 | return (string) $v; 49 | }, (array) $val); 50 | default: 51 | assert(false, 'Unsupported typecast '.$type); 52 | 53 | return $val; 54 | } 55 | } 56 | 57 | public static function normalizeType(string $type): string 58 | { 59 | switch ($type) { 60 | case 'arr': 61 | case 'array': 62 | return 'array'; 63 | case 'arrOfInt': 64 | case 'arrayOfInt': 65 | return 'arrayOfInt'; 66 | case 'arrOfStr': 67 | case 'arrayOfString': 68 | return 'arrayOfString'; 69 | case 'bool': 70 | case 'boolean': 71 | return 'boolean'; 72 | case 'str': 73 | case 'string': 74 | return 'string'; 75 | case 'int': 76 | case 'integer': 77 | return 'integer'; 78 | case 'float': 79 | case 'double': 80 | return 'float'; 81 | default: 82 | throw new \LogicException("Unsupported type: $type"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Utils/ArrayHelper.php: -------------------------------------------------------------------------------- 1 | ['type' => 'int', 'requiredCreate' => false, 'requiredUpdate' => true], 7 | 'barcode' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 8 | 'description' => ['type' => 'str', 'requiredCreate' => true, 'requiredUpdate' => false], 9 | 'category_id' => ['type' => 'int', 'requiredCreate' => true, 'requiredUpdate' => false], 10 | 'name' => ['type' => 'str', 'requiredCreate' => true, 'requiredUpdate' => false], 11 | 'offer_id' => ['type' => 'str', 'requiredCreate' => true, 'requiredUpdate' => false], 12 | 'price' => ['type' => 'str', 'requiredCreate' => true, 'requiredUpdate' => false], 13 | 'old_price' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 14 | 'premium_price' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 15 | 'vat' => ['type' => 'str', 'requiredCreate' => true, 'requiredUpdate' => false], 16 | 'vendor' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 17 | 'vendor_code' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 18 | 'attributes' => ['type' => 'array', 'requiredCreate' => false, 'requiredUpdate' => false], 19 | 'image_group_id' => ['type' => 'str', 'requiredCreate' => false, 'requiredUpdate' => false], 20 | 'images' => ['type' => 'array', 'requiredCreate' => true, 'requiredUpdate' => false], 21 | 'images360' => ['type' => 'array', 'requiredCreate' => false, 'requiredUpdate' => false], 22 | 'pdf_list' => ['type' => 'array', 'requiredCreate' => false, 'requiredUpdate' => false], 23 | 'height' => ['type' => 'int', 'requiredCreate' => true, 'requiredUpdate' => false], 24 | 'depth' => ['type' => 'int', 'requiredCreate' => true, 'requiredUpdate' => false], 25 | 'width' => ['type' => 'int', 'requiredCreate' => true, 'requiredUpdate' => false], 26 | 'dimension_unit' => [ 27 | 'type' => 'str', 28 | 'requiredCreate' => true, 29 | 'requiredUpdate' => false, 30 | 'options' => ['mm', 'cm', 'in'], 31 | ], 32 | 'weight' => ['type' => 'int', 'requiredCreate' => true, 'requiredUpdate' => false], 33 | 'weight_unit' => [ 34 | 'type' => 'str', 35 | 'requiredCreate' => true, 36 | 'requiredUpdate' => false, 37 | 'options' => ['g', 'kg', 'lb'], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /src/config/product_validator_v2.php: -------------------------------------------------------------------------------- 1 | ['type' => 'string', 'requiredCreate' => false], 7 | 8 | 'offer_id' => ['type' => 'string', 'requiredCreate' => true], 9 | 10 | 'attributes' => [ 11 | 'type' => 'array', // todo type attributes array 12 | 'requiredCreate' => true, 13 | ], 14 | 'complex_attributes' => ['type' => 'array', 'requiredCreate' => false], 15 | 16 | 'barcode' => ['type' => 'str', 'requiredCreate' => false], 17 | 'category_id' => ['type' => 'int', 'requiredCreate' => true], 18 | 19 | 'width' => ['type' => 'int', 'requiredCreate' => true], 20 | 'height' => ['type' => 'int', 'requiredCreate' => true], 21 | 'depth' => ['type' => 'int', 'requiredCreate' => true], 22 | 'dimension_unit' => [ 23 | 'type' => 'str', 24 | 'requiredCreate' => true, 25 | 'options' => ['mm', 'cm', 'in'], 26 | ], 27 | 28 | 'weight' => ['type' => 'int', 'requiredCreate' => true], 29 | 'weight_unit' => [ 30 | 'type' => 'str', 31 | 'requiredCreate' => true, 32 | 'options' => ['g', 'kg', 'lb'], 33 | ], 34 | 35 | 'primary_image' => ['type' => 'str', 'requiredCreate' => false], 36 | 'image_group_id' => ['type' => 'str', 'requiredCreate' => false], 37 | 'images' => ['type' => 'array', 'requiredCreate' => true], 38 | 'images360' => [ 39 | 'type' => 'array', // todo type image360 40 | 'requiredCreate' => false, 41 | ], 42 | 43 | 'pdf_list' => [ 44 | 'type' => 'array', // todo type pdf_list 45 | 'requiredCreate' => false, 46 | ], 47 | 48 | 'old_price' => ['type' => 'str', 'requiredCreate' => false], 49 | 'price' => ['type' => 'str', 'requiredCreate' => true], 50 | 'premium_price' => ['type' => 'str', 'requiredCreate' => false], 51 | 'vat' => ['type' => 'str', 'requiredCreate' => true], 52 | ]; 53 | --------------------------------------------------------------------------------