Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
ProductRepositoryInterfaceTest.php
Go to the documentation of this file.
1 <?php
6 declare(strict_types=1);
7 
8 namespace Magento\Catalog\Api;
9 
24 use Magento\Framework\Webapi\Exception as HTTPExceptionCodes;
25 
31 {
32  const SERVICE_NAME = 'catalogProductRepositoryV1';
33  const SERVICE_VERSION = 'V1';
34  const RESOURCE_PATH = '/V1/products';
35 
36  const KEY_TIER_PRICES = 'tier_prices';
37  const KEY_SPECIAL_PRICE = 'special_price';
38  const KEY_CATEGORY_LINKS = 'category_links';
39 
43  private $productData = [
44  [
45  ProductInterface::SKU => 'simple',
46  ProductInterface::NAME => 'Simple Related Product',
47  ProductInterface::TYPE_ID => 'simple',
49  ],
50  [
51  ProductInterface::SKU => 'simple_with_cross',
52  ProductInterface::NAME => 'Simple Product With Related Product',
53  ProductInterface::TYPE_ID => 'simple',
55  ],
56  ];
57 
61  public function testGet()
62  {
63  $productData = $this->productData[0];
64  $response = $this->getProduct($productData[ProductInterface::SKU]);
66  $this->assertEquals($productData[$key], $response[$key]);
67  }
68  $this->assertEquals([1], $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"]);
69  }
70 
76  protected function getProduct($sku, $storeCode = null)
77  {
78  $serviceInfo = [
79  'rest' => [
80  'resourcePath' => self::RESOURCE_PATH . '/' . $sku,
82  ],
83  'soap' => [
84  'service' => self::SERVICE_NAME,
85  'serviceVersion' => self::SERVICE_VERSION,
86  'operation' => self::SERVICE_NAME . 'Get',
87  ],
88  ];
89 
90  $response = $this->_webApiCall($serviceInfo, ['sku' => $sku], null, $storeCode);
91  return $response;
92  }
93 
94  public function testGetNoSuchEntityException()
95  {
96  $invalidSku = '(nonExistingSku)';
97  $serviceInfo = [
98  'rest' => [
99  'resourcePath' => self::RESOURCE_PATH . '/' . $invalidSku,
101  ],
102  'soap' => [
103  'service' => self::SERVICE_NAME,
104  'serviceVersion' => self::SERVICE_VERSION,
105  'operation' => self::SERVICE_NAME . 'Get',
106  ],
107  ];
108 
109  $expectedMessage = "The product that was requested doesn't exist. Verify the product and try again.";
110 
111  try {
112  $this->_webApiCall($serviceInfo, ['sku' => $invalidSku]);
113  $this->fail("Expected throwing exception");
114  } catch (\SoapFault $e) {
115  $this->assertContains(
116  $expectedMessage,
117  $e->getMessage(),
118  "SoapFault does not contain expected message."
119  );
120  } catch (\Exception $e) {
121  $errorObj = $this->processRestExceptionResult($e);
122  $this->assertEquals($expectedMessage, $errorObj['message']);
123  $this->assertEquals(HTTPExceptionCodes::HTTP_NOT_FOUND, $e->getCode());
124  }
125  }
126 
130  public function productCreationProvider()
131  {
132  $productBuilder = function ($data) {
133  return array_replace_recursive(
134  $this->getSimpleProductData(),
135  $data
136  );
137  };
138  return [
139  [$productBuilder([ProductInterface::TYPE_ID => 'simple', ProductInterface::SKU => 'psku-test-1'])],
140  [$productBuilder([ProductInterface::TYPE_ID => 'virtual', ProductInterface::SKU => 'psku-test-2'])],
141  ];
142  }
143 
150  private function loadWebsiteByCode($websiteCode)
151  {
152  $websiteRepository = Bootstrap::getObjectManager()->get(WebsiteRepository::class);
153  try {
155  } catch (NoSuchEntityException $e) {
156  $this->fail("Couldn`t load website: {$websiteCode}");
157  }
158 
159  return $website;
160  }
161 
166  public function testUpdateWithDeleteWebsites()
167  {
168  $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza';
170  $website = $this->loadWebsiteByCode('second_website');
171 
172  $websitesData = [
173  'website_ids' => [
174  $website->getId(),
175  ]
176  ];
177  $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData;
178  $response = $this->updateProduct($productBuilder);
179  $this->assertEquals(
181  $websitesData["website_ids"]
182  );
183  }
184 
190  {
191  $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza';
192 
193  $websitesData = [
194  'website_ids' => []
195  ];
196  $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData;
197  $response = $this->updateProduct($productBuilder);
198  $this->assertEquals(
200  $websitesData["website_ids"]
201  );
202  }
203 
207  public function testCreateWithMultipleWebsites()
208  {
209  $productBuilder = $this->getSimpleProductData();
210  $productBuilder[ProductInterface::SKU] = 'test-test-sku';
211  $productBuilder[ProductInterface::TYPE_ID] = 'simple';
213  $website = $this->loadWebsiteByCode('test_website');
214 
215  $websitesData = [
216  'website_ids' => [
217  1,
218  (int) $website->getId(),
219  ]
220  ];
221  $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData;
222  $response = $this->saveProduct($productBuilder);
223  $this->assertEquals(
225  $websitesData["website_ids"]
226  );
227  $this->deleteProduct($productBuilder[ProductInterface::SKU]);
228  }
229 
235  public function testCreateWithNonDefaultStoreWebsite()
236  {
237  $productBuilder = $this->getSimpleProductData();
238  $productBuilder[ProductInterface::SKU] = 'test-sku-second-site-123';
239  $productBuilder[ProductInterface::TYPE_ID] = 'simple';
241  $website = $this->loadWebsiteByCode('test');
242 
243  $websitesData = [
244  'website_ids' => [
245  $website->getId(),
246  ]
247  ];
248  $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData;
249  $response = $this->saveProduct($productBuilder);
250  $this->assertEquals(
251  $websitesData["website_ids"],
253  );
254  $this->deleteProduct($productBuilder[ProductInterface::SKU]);
255  }
256 
263  public function testUpdateWithNonDefaultStoreWebsite()
264  {
265  $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza';
267  $website = $this->loadWebsiteByCode('test');
268 
269  $this->assertNotContains(Store::SCOPE_DEFAULT, $website->getStoreCodes());
270 
271  $websitesData = [
272  'website_ids' => [
273  $website->getId(),
274  ]
275  ];
276  $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData;
277  $response = $this->updateProduct($productBuilder);
278  $this->assertEquals(
279  $websitesData["website_ids"],
281  );
282  }
283 
289  public function testUpdateWithoutWebsiteIds()
290  {
291  $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza';
292  $originalProduct = $this->getProduct($productBuilder[ProductInterface::SKU]);
293  $newName = 'Updated Product';
294 
295  $productBuilder[ProductInterface::NAME] = $newName;
296  $response = $this->updateProduct($productBuilder);
297  $this->assertEquals(
298  $newName,
300  );
301  $this->assertEquals(
302  $originalProduct[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"],
304  );
305  }
306 
310  public function testCreate($product)
311  {
312  $response = $this->saveProduct($product);
313  $this->assertArrayHasKey(ProductInterface::SKU, $response);
315  }
316 
323  public function testCreateAllStoreCode($fixtureProduct)
324  {
325  $response = $this->saveProduct($fixtureProduct, 'all');
326  $this->assertArrayHasKey(ProductInterface::SKU, $response);
327 
330  \Magento\Store\Model\StoreManagerInterface::class
331  );
332 
333  foreach ($storeManager->getStores(true) as $store) {
334  $code = $store->getCode();
335  if ($code === Store::ADMIN_CODE) {
336  continue;
337  }
338  $this->assertArrayHasKey(
340  $this->getProduct($fixtureProduct[ProductInterface::SKU], $code)
341  );
342  }
343  $this->deleteProduct($fixtureProduct[ProductInterface::SKU]);
344  }
345 
352  public function testCreateAllStoreCodeForSingleWebsite($fixtureProduct)
353  {
354  $response = $this->saveProduct($fixtureProduct, 'all');
355  $this->assertArrayHasKey(ProductInterface::SKU, $response);
356 
359  \Magento\Store\Model\StoreManagerInterface::class
360  );
361 
362  foreach ($storeManager->getStores(true) as $store) {
363  $code = $store->getCode();
364  if ($code === Store::ADMIN_CODE) {
365  continue;
366  }
367  $this->assertArrayHasKey(
369  $this->getProduct($fixtureProduct[ProductInterface::SKU], $code)
370  );
371  }
372  $this->deleteProduct($fixtureProduct[ProductInterface::SKU]);
373  }
374 
376  {
377  $this->_markTestAsRestOnly("In case of SOAP type casting is handled by PHP SoapServer, no need to test it");
378  $expectedMessage = 'Error occurred during "price" processing. '
379  . 'The "invalid_format" value\'s type is invalid. The "float" type was expected. Verify and try again.';
380 
381  try {
382  $this->saveProduct(['name' => 'simple', 'price' => 'invalid_format', 'sku' => 'simple']);
383  $this->fail("Expected exception was not raised");
384  } catch (\Exception $e) {
385  $errorObj = $this->processRestExceptionResult($e);
386  $this->assertEquals($expectedMessage, $errorObj['message']);
387  $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode());
388  }
389  }
390 
397  public function testDeleteAllStoreCode($fixtureProduct)
398  {
399  $sku = $fixtureProduct[ProductInterface::SKU];
400  $this->saveProduct($fixtureProduct);
401  $this->expectException('Exception');
402  $this->expectExceptionMessage(
403  "The product that was requested doesn't exist. Verify the product and try again."
404  );
405 
406  // Delete all with 'all' store code
407  $this->deleteProduct($sku);
408  $this->getProduct($sku);
409  }
410 
411  public function testProductLinks()
412  {
413  // Create simple product
414  $productData = [
415  ProductInterface::SKU => "product_simple_500",
416  ProductInterface::NAME => "Product Simple 500",
418  ProductInterface::TYPE_ID => 'simple',
421  ProductInterface::TYPE_ID => 'simple',
424  'stock_item' => $this->getStockItemData()
425  ]
426  ];
427 
428  $this->saveProduct($productData);
429 
430  $productLinkData = [
431  "sku" => "product_simple_with_related_500",
432  "link_type" => "related",
433  "linked_product_sku" => "product_simple_500",
434  "linked_product_type" => "simple",
435  "position" => 0
436  ];
437  $productWithRelatedData = [
438  ProductInterface::SKU => "product_simple_with_related_500",
439  ProductInterface::NAME => "Product Simple with Related 500",
441  ProductInterface::TYPE_ID => 'simple',
444  ProductInterface::TYPE_ID => 'simple',
446  "product_links" => [$productLinkData]
447  ];
448 
449  $this->saveProduct($productWithRelatedData);
450  $response = $this->getProduct("product_simple_with_related_500");
451 
452  $this->assertArrayHasKey('product_links', $response);
453  $links = $response['product_links'];
454  $this->assertEquals(1, count($links));
455  $this->assertEquals($productLinkData, $links[0]);
456 
457  // update link information
458  $productLinkData = [
459  "sku" => "product_simple_with_related_500",
460  "link_type" => "upsell",
461  "linked_product_sku" => "product_simple_500",
462  "linked_product_type" => "simple",
463  "position" => 0
464  ];
465  $productWithUpsellData = [
466  ProductInterface::SKU => "product_simple_with_related_500",
467  ProductInterface::NAME => "Product Simple with Related 500",
469  ProductInterface::TYPE_ID => 'simple',
473  "product_links" => [$productLinkData]
474  ];
475 
476  $this->saveProduct($productWithUpsellData);
477  $response = $this->getProduct("product_simple_with_related_500");
478 
479  $this->assertArrayHasKey('product_links', $response);
480  $links = $response['product_links'];
481  $this->assertEquals(1, count($links));
482  $this->assertEquals($productLinkData, $links[0]);
483 
484  // Remove link
485  $productWithNoLinkData = [
486  ProductInterface::SKU => "product_simple_with_related_500",
487  ProductInterface::NAME => "Product Simple with Related 500",
489  ProductInterface::TYPE_ID => 'simple',
493  "product_links" => []
494  ];
495 
496  $this->saveProduct($productWithNoLinkData);
497  $response = $this->getProduct("product_simple_with_related_500");
498  $this->assertArrayHasKey('product_links', $response);
499  $links = $response['product_links'];
500  $this->assertEquals([], $links);
501 
502  $this->deleteProduct("product_simple_500");
503  $this->deleteProduct("product_simple_with_related_500");
504  }
505 
510  protected function getOptionsData($productSku)
511  {
512  return [
513  [
514  "product_sku" => $productSku,
515  "title" => "DropdownOption",
516  "type" => "drop_down",
517  "sort_order" => 0,
518  "is_require" => true,
519  "values" => [
520  [
521  "title" => "DropdownOption2_1",
522  "sort_order" => 0,
523  "price" => 3,
524  "price_type" => "fixed",
525  ],
526  ],
527  ],
528  [
529  "product_sku" => $productSku,
530  "title" => "CheckboxOption",
531  "type" => "checkbox",
532  "sort_order" => 1,
533  "is_require" => false,
534  "values" => [
535  [
536  "title" => "CheckBoxValue1",
537  "price" => 5,
538  "price_type" => "fixed",
539  "sort_order" => 1,
540  ],
541  ],
542  ],
543  ];
544  }
545 
546  public function testProductOptions()
547  {
548  //Create product with options
549  $productData = $this->getSimpleProductData();
550  $optionsDataInput = $this->getOptionsData($productData['sku']);
551  $productData['options'] = $optionsDataInput;
552  $this->saveProduct($productData);
553  $response = $this->getProduct($productData[ProductInterface::SKU]);
554 
555  $this->assertArrayHasKey('options', $response);
556  $options = $response['options'];
557  $this->assertEquals(2, count($options));
558  $this->assertEquals(1, count($options[0]['values']));
559  $this->assertEquals(1, count($options[1]['values']));
560 
561  //update the product options, adding a value to option 1, delete an option and create a new option
562  $options[0]['values'][] = [
563  "title" => "Value2",
564  "price" => 6,
565  "price_type" => "fixed",
566  'sort_order' => 3,
567  ];
568  $options[1] = [
569  "product_sku" => $productData['sku'],
570  "title" => "DropdownOption2",
571  "type" => "drop_down",
572  "sort_order" => 3,
573  "is_require" => false,
574  "values" => [
575  [
576  "title" => "Value3",
577  "price" => 7,
578  "price_type" => "fixed",
579  "sort_order" => 4,
580  ],
581  ],
582  ];
583  $response['options'] = $options;
585  $this->assertArrayHasKey('options', $response);
586  $options = $response['options'];
587  $this->assertEquals(2, count($options));
588  $this->assertEquals(2, count($options[0]['values']));
589  $this->assertEquals(1, count($options[1]['values']));
590 
591  //update product without setting options field, option should not be changed
592  unset($response['options']);
593  $this->updateProduct($response);
594  $response = $this->getProduct($productData[ProductInterface::SKU]);
595  $this->assertArrayHasKey('options', $response);
596  $options = $response['options'];
597  $this->assertEquals(2, count($options));
598 
599  //update product with empty options, options should be removed
600  $response['options'] = [];
602  $this->assertEmpty($response['options']);
603 
604  $this->deleteProduct($productData[ProductInterface::SKU]);
605  }
606 
607  public function testProductWithMediaGallery()
608  {
609  $testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg';
610  $encodedImage = base64_encode(file_get_contents($testImagePath));
611  //create a product with media gallery
612  $filename1 = 'tiny1' . time() . '.jpg';
613  $filename2 = 'tiny2' . time() . '.jpeg';
614  $productData = $this->getSimpleProductData();
615  $productData['media_gallery_entries'] = $this->getMediaGalleryData($filename1, $encodedImage, $filename2);
616  $response = $this->saveProduct($productData);
617  $this->assertArrayHasKey('media_gallery_entries', $response);
618  $mediaGalleryEntries = $response['media_gallery_entries'];
619  $this->assertEquals(2, count($mediaGalleryEntries));
620  $id = $mediaGalleryEntries[0]['id'];
621  foreach ($mediaGalleryEntries as &$entry) {
622  unset($entry['id']);
623  }
624  $expectedValue = [
625  [
626  'label' => 'tiny1',
627  'position' => 1,
628  'media_type' => 'image',
629  'disabled' => true,
630  'types' => [],
631  'file' => '/t/i/' . $filename1,
632  ],
633  [
634  'label' => 'tiny2',
635  'position' => 2,
636  'media_type' => 'image',
637  'disabled' => false,
638  'types' => ['image', 'small_image'],
639  'file' => '/t/i/' . $filename2,
640  ],
641  ];
642  $this->assertEquals($expectedValue, $mediaGalleryEntries);
643  //update the product media gallery
644  $response['media_gallery_entries'] = [
645  [
646  'id' => $id,
647  'media_type' => 'image',
648  'label' => 'tiny1_new_label',
649  'position' => 1,
650  'disabled' => false,
651  'types' => ['image', 'small_image'],
652  'file' => '/t/i/' . $filename1,
653  ],
654  ];
656  $mediaGalleryEntries = $response['media_gallery_entries'];
657  $this->assertEquals(1, count($mediaGalleryEntries));
658  unset($mediaGalleryEntries[0]['id']);
659  $expectedValue = [
660  [
661  'label' => 'tiny1_new_label',
662  'media_type' => 'image',
663  'position' => 1,
664  'disabled' => false,
665  'types' => ['image', 'small_image'],
666  'file' => '/t/i/' . $filename1,
667  ]
668  ];
669  $this->assertEquals($expectedValue, $mediaGalleryEntries);
670  //don't set the media_gallery_entries field, existing entry should not be touched
671  unset($response['media_gallery_entries']);
673  $mediaGalleryEntries = $response['media_gallery_entries'];
674  $this->assertEquals(1, count($mediaGalleryEntries));
675  unset($mediaGalleryEntries[0]['id']);
676  $this->assertEquals($expectedValue, $mediaGalleryEntries);
677  //pass empty array, delete all existing media gallery entries
678  $response['media_gallery_entries'] = [];
680  $this->assertEquals(true, empty($response['media_gallery_entries']));
681  $this->deleteProduct($productData[ProductInterface::SKU]);
682  }
683 
687  public function testUpdate()
688  {
689  $productData = [
690  ProductInterface::NAME => 'Very Simple Product', //new name
691  ProductInterface::SKU => 'simple', //sku from fixture
692  ];
693  $product = $this->getSimpleProductData($productData);
694  $response = $this->updateProduct($product);
695 
696  $this->assertArrayHasKey(ProductInterface::SKU, $response);
697  $this->assertArrayHasKey(ProductInterface::NAME, $response);
698  $this->assertEquals($productData[ProductInterface::NAME], $response[ProductInterface::NAME]);
699  $this->assertEquals($productData[ProductInterface::SKU], $response[ProductInterface::SKU]);
700  }
701 
707  public function testUpdateWithExtensionAttributes(): void
708  {
709  $sku = 'downloadable-product';
710  $linksKey = 'downloadable_product_links';
711  $productData = [
712  ProductInterface::NAME => 'Downloadable (updated)',
713  ProductInterface::SKU => $sku,
714  ];
715  $response = $this->updateProduct($productData);
716 
718  self::assertArrayHasKey($linksKey, $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]);
719  self::assertCount(1, $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY][$linksKey]);
720 
722 
723  self::assertArrayHasKey(Link::KEY_LINK_URL, $linkData);
724  self::assertEquals('http://example.com/downloadable.txt', $linkData[Link::KEY_LINK_URL]);
725  }
726 
731  protected function updateProduct($product)
732  {
733  if (isset($product['custom_attributes'])) {
734  for ($i=0; $i<sizeof($product['custom_attributes']); $i++) {
735  if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids'
736  && !is_array($product['custom_attributes'][$i]['value'])
737  ) {
738  $product['custom_attributes'][$i]['value'] = [""];
739  }
740  }
741  }
743  if (TESTS_WEB_API_ADAPTER == self::ADAPTER_REST) {
745  }
746 
747  $serviceInfo = [
748  'rest' => [
749  'resourcePath' => self::RESOURCE_PATH . '/' . $sku,
751  ],
752  'soap' => [
753  'service' => self::SERVICE_NAME,
754  'serviceVersion' => self::SERVICE_VERSION,
755  'operation' => self::SERVICE_NAME . 'Save',
756  ],
757  ];
758  $requestData = ['product' => $product];
759  $response = $this->_webApiCall($serviceInfo, $requestData);
760  return $response;
761  }
762 
766  public function testDelete()
767  {
768  $response = $this->deleteProduct('simple');
769  $this->assertTrue($response);
770  }
771 
775  public function testGetList()
776  {
777  $searchCriteria = [
778  'searchCriteria' => [
779  'filter_groups' => [
780  [
781  'filters' => [
782  [
783  'field' => 'sku',
784  'value' => 'simple',
785  'condition_type' => 'eq',
786  ],
787  ],
788  ],
789  ],
790  'current_page' => 1,
791  'page_size' => 2,
792  ],
793  ];
794 
795  $serviceInfo = [
796  'rest' => [
797  'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($searchCriteria),
799  ],
800  'soap' => [
801  'service' => self::SERVICE_NAME,
802  'serviceVersion' => self::SERVICE_VERSION,
803  'operation' => self::SERVICE_NAME . 'GetList',
804  ],
805  ];
806 
807  $response = $this->_webApiCall($serviceInfo, $searchCriteria);
808 
809  $this->assertArrayHasKey('search_criteria', $response);
810  $this->assertArrayHasKey('total_count', $response);
811  $this->assertArrayHasKey('items', $response);
812 
813  $this->assertEquals($searchCriteria['searchCriteria'], $response['search_criteria']);
814  $this->assertTrue($response['total_count'] > 0);
815  $this->assertTrue(count($response['items']) > 0);
816 
817  $this->assertNotNull($response['items'][0]['sku']);
818  $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']);
819  $this->assertEquals('simple', $response['items'][0]['sku']);
820 
821  $index = null;
822  foreach ($response['items'][0]['custom_attributes'] as $key => $customAttribute) {
823  if ($customAttribute['attribute_code'] == 'category_ids') {
824  $index = $key;
825  break;
826  }
827  }
828  $this->assertNotNull($index, 'Category information wasn\'t set');
829 
830  $expectedResult = (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) ? ['string' => '2'] : ['2'];
831  $this->assertEquals($expectedResult, $response['items'][0]['custom_attributes'][$index]['value']);
832  }
833 
838  {
839  $this->_markTestAsRestOnly();
840  $searchCriteria = [
841  'searchCriteria' => [
842  'current_page' => 1,
843  'page_size' => 2,
844  ],
845  ];
846  $additionalParams = urlencode('items[id,custom_attributes[description]]');
847 
848  $serviceInfo = [
849  'rest' => [
850  'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($searchCriteria) . '&fields=' .
851  $additionalParams,
853  ]
854  ];
855 
856  $response = $this->_webApiCall($serviceInfo, $searchCriteria);
857 
858  $this->assertArrayHasKey('items', $response);
859  $this->assertTrue(count($response['items']) > 0);
860 
861  $indexDescription = null;
862  foreach ($response['items'][0]['custom_attributes'] as $key => $customAttribute) {
863  if ($customAttribute['attribute_code'] == 'description') {
864  $indexDescription = $key;
865  }
866  }
867 
868  $this->assertNotNull($response['items'][0]['custom_attributes'][$indexDescription]['attribute_code']);
869  $this->assertNotNull($response['items'][0]['custom_attributes'][$indexDescription]['value']);
870  $this->assertTrue(count($response['items'][0]['custom_attributes']) == 1);
871  }
872 
878  {
879  $website = $this->loadWebsiteByCode('test');
880  $searchCriteria = [
881  'searchCriteria' => [
882  'filter_groups' => [
883  [
884  'filters' => [
885  [
886  'field' => 'website_id',
887  'value' => $website->getId(),
888  'condition_type' => 'eq',
889  ],
890  ],
891  ],
892  ],
893  'current_page' => 1,
894  'page_size' => 10,
895  ],
896  ];
897  $serviceInfo = [
898  'rest' => [
899  'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($searchCriteria),
901  ],
902  'soap' => [
903  'service' => self::SERVICE_NAME,
904  'serviceVersion' => self::SERVICE_VERSION,
905  'operation' => self::SERVICE_NAME . 'GetList',
906  ],
907  ];
908  $response = $this->_webApiCall($serviceInfo, $searchCriteria);
909 
910  $this->assertArrayHasKey('search_criteria', $response);
911  $this->assertArrayHasKey('total_count', $response);
912  $this->assertArrayHasKey('items', $response);
913  $this->assertTrue(count($response['items']) == 1);
914  $this->assertTrue(isset($response['items'][0]['sku']));
915  $this->assertEquals('simple-2', $response['items'][0]['sku']);
916  $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']);
917  }
918 
928  public function testGetListWithFilteringByStore(array $searchCriteria, array $skus, $expectedProductCount = null)
929  {
930  $serviceInfo = [
931  'rest' => [
932  'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($searchCriteria),
934  ],
935  'soap' => [
936  'service' => self::SERVICE_NAME,
937  'serviceVersion' => self::SERVICE_VERSION,
938  'operation' => self::SERVICE_NAME . 'GetList',
939  ],
940  ];
941  $response = $this->_webApiCall($serviceInfo, $searchCriteria);
942 
943  $this->assertArrayHasKey('search_criteria', $response);
944  $this->assertArrayHasKey('total_count', $response);
945  $this->assertArrayHasKey('items', $response);
946  if ($expectedProductCount) {
947  $this->assertTrue(count($response['items']) == $expectedProductCount);
948  }
949 
950  $isResultValid = false;
951  foreach ($skus as $sku) {
952  foreach ($response['items'] as $item) {
953  if ($item['sku'] == $sku) {
954  $isResultValid = true;
955  }
956  }
957  $this->assertTrue($isResultValid);
958  }
959  }
960 
962  {
963  return [
964  [
965  [
966  'searchCriteria' => [
967  'filter_groups' => [
968  [
969  'filters' => [
970  [
971  'field' => 'store',
972  'value' => 'fixture_second_store',
973  'condition_type' => 'eq',
974  ],
975  ],
976  ],
977  ],
978  'current_page' => 1,
979  'page_size' => 10,
980  ],
981  ],
982  ['simple-2'],
983  1,
984  ],
985  [
986  [
987  'searchCriteria' => [
988  'filter_groups' => [],
989  'current_page' => 1,
990  'page_size' => 10,
991  ],
992  ],
993  ['simple-2', 'simple-1'],
994  null
995  ]
996  ];
997  }
998 
1002  public function testGetListWithMultipleFilterGroupsAndSortingAndPagination()
1003  {
1005  $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class);
1006 
1007  $filter1 = $filterBuilder->setField(ProductInterface::NAME)
1008  ->setValue('search product 2')
1009  ->create();
1010  $filter2 = $filterBuilder->setField(ProductInterface::NAME)
1011  ->setValue('search product 3')
1012  ->create();
1013  $filter3 = $filterBuilder->setField(ProductInterface::NAME)
1014  ->setValue('search product 4')
1015  ->create();
1016  $filter4 = $filterBuilder->setField(ProductInterface::NAME)
1017  ->setValue('search product 5')
1018  ->create();
1019  $filter5 = $filterBuilder->setField(ProductInterface::PRICE)
1020  ->setValue(35)
1021  ->setConditionType('lt')
1022  ->create();
1023  $filter6 = $filterBuilder->setField('category_id')
1024  ->setValue(333)
1025  ->create();
1026 
1028  $sortOrderBuilder = Bootstrap::getObjectManager()->create(SortOrderBuilder::class);
1029 
1031  $sortOrder = $sortOrderBuilder->setField('meta_title')->setDirection(SortOrder::SORT_DESC)->create();
1032 
1034  $searchCriteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class);
1035 
1036  $searchCriteriaBuilder->addFilters([$filter1, $filter2, $filter3, $filter4]);
1037  $searchCriteriaBuilder->addFilters([$filter5]);
1038  $searchCriteriaBuilder->addFilters([$filter6]);
1039  $searchCriteriaBuilder->setSortOrders([$sortOrder]);
1040 
1041  $searchCriteriaBuilder->setPageSize(2);
1042  $searchCriteriaBuilder->setCurrentPage(2);
1043 
1044  $searchData = $searchCriteriaBuilder->create()->__toArray();
1045  $requestData = ['searchCriteria' => $searchData];
1046  $serviceInfo = [
1047  'rest' => [
1048  'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData),
1050  ],
1051  'soap' => [
1052  'service' => self::SERVICE_NAME,
1053  'serviceVersion' => self::SERVICE_VERSION,
1054  'operation' => self::SERVICE_NAME . 'GetList',
1055  ],
1056  ];
1057 
1058  $searchResult = $this->_webApiCall($serviceInfo, $requestData);
1059 
1060  $this->assertEquals(3, $searchResult['total_count']);
1061  $this->assertEquals(1, count($searchResult['items']));
1062  $this->assertEquals('search_product_4', $searchResult['items'][0][ProductInterface::SKU]);
1063  $this->assertNotNull(
1064  $searchResult['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']
1065  );
1066  }
1067 
1072  protected function convertCustomAttributesToAssociativeArray($customAttributes)
1073  {
1074  $converted = [];
1075  foreach ($customAttributes as $customAttribute) {
1076  $converted[$customAttribute['attribute_code']] = $customAttribute['value'];
1077  }
1078  return $converted;
1079  }
1080 
1086  {
1087  $customAttributes = [];
1088  foreach ($data as $attributeCode => $attributeValue) {
1089  $customAttributes[] = ['attribute_code' => $attributeCode, 'value' => $attributeValue];
1090  }
1091  return $customAttributes;
1092  }
1093 
1097  public function testEavAttributes()
1098  {
1099  $response = $this->getProduct('simple');
1100 
1101  $this->assertNotEmpty($response['custom_attributes']);
1102  $customAttributesData = $this->convertCustomAttributesToAssociativeArray($response['custom_attributes']);
1103  $this->assertNotTrue(isset($customAttributesData['name']));
1104  $this->assertNotTrue(isset($customAttributesData['tier_price']));
1105 
1106  //Set description
1107  $descriptionValue = "new description";
1108  $customAttributesData['description'] = $descriptionValue;
1109  $response['custom_attributes'] = $this->convertAssociativeArrayToCustomAttributes($customAttributesData);
1110 
1111  $response = $this->updateProduct($response);
1112  $this->assertNotEmpty($response['custom_attributes']);
1113 
1114  $customAttributesData = $this->convertCustomAttributesToAssociativeArray($response['custom_attributes']);
1115  $this->assertTrue(isset($customAttributesData['description']));
1116  $this->assertEquals($descriptionValue, $customAttributesData['description']);
1117 
1118  $this->deleteProduct('simple');
1119  }
1120 
1127  protected function getSimpleProductData($productData = [])
1128  {
1129  return [
1130  ProductInterface::SKU => isset($productData[ProductInterface::SKU])
1131  ? $productData[ProductInterface::SKU] : uniqid('sku-', true),
1132  ProductInterface::NAME => isset($productData[ProductInterface::NAME])
1133  ? $productData[ProductInterface::NAME] : uniqid('sku-', true),
1135  ProductInterface::TYPE_ID => 'simple',
1136  ProductInterface::PRICE => 3.62,
1138  ProductInterface::TYPE_ID => 'simple',
1140  'custom_attributes' => [
1141  ['attribute_code' => 'cost', 'value' => ''],
1142  ['attribute_code' => 'description', 'value' => 'Description'],
1143  ]
1144  ];
1145  }
1146 
1152  protected function saveProduct($product, $storeCode = null)
1153  {
1154  if (isset($product['custom_attributes'])) {
1155  for ($i=0; $i<sizeof($product['custom_attributes']); $i++) {
1156  if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids'
1157  && !is_array($product['custom_attributes'][$i]['value'])
1158  ) {
1159  $product['custom_attributes'][$i]['value'] = [""];
1160  }
1161  }
1162  }
1163  $serviceInfo = [
1164  'rest' => [
1165  'resourcePath' => self::RESOURCE_PATH,
1167  ],
1168  'soap' => [
1169  'service' => self::SERVICE_NAME,
1170  'serviceVersion' => self::SERVICE_VERSION,
1171  'operation' => self::SERVICE_NAME . 'Save',
1172  ],
1173  ];
1174  $requestData = ['product' => $product];
1175  return $this->_webApiCall($serviceInfo, $requestData, null, $storeCode);
1176  }
1177 
1184  protected function deleteProduct($sku)
1185  {
1186  $serviceInfo = [
1187  'rest' => [
1188  'resourcePath' => self::RESOURCE_PATH . '/' . $sku,
1190  ],
1191  'soap' => [
1192  'service' => self::SERVICE_NAME,
1193  'serviceVersion' => self::SERVICE_VERSION,
1194  'operation' => self::SERVICE_NAME . 'DeleteById',
1195  ],
1196  ];
1197 
1198  return (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) ?
1199  $this->_webApiCall($serviceInfo, ['sku' => $sku]) : $this->_webApiCall($serviceInfo);
1200  }
1201 
1202  public function testTierPrices()
1203  {
1204  // create a product with tier prices
1207  $productData = $this->getSimpleProductData();
1208  $productData[self::KEY_TIER_PRICES] = [
1209  [
1210  'customer_group_id' => $custGroup1,
1211  'value' => 3.14,
1212  'qty' => 5,
1213  ],
1214  [
1215  'customer_group_id' => $custGroup2,
1216  'value' => 3.45,
1217  'qty' => 10,
1218  ]
1219  ];
1220  $this->saveProduct($productData);
1221  $response = $this->getProduct($productData[ProductInterface::SKU]);
1222 
1223  $this->assertArrayHasKey(self::KEY_TIER_PRICES, $response);
1225  $this->assertNotNull($tierPrices, "CREATE: expected to have tier prices");
1226  $this->assertCount(2, $tierPrices, "CREATE: expected to have 2 'tier_prices' objects");
1227  $this->assertEquals(3.14, $tierPrices[0]['value']);
1228  $this->assertEquals(5, $tierPrices[0]['qty']);
1229  $this->assertEquals($custGroup1, $tierPrices[0]['customer_group_id']);
1230  $this->assertEquals(3.45, $tierPrices[1]['value']);
1231  $this->assertEquals(10, $tierPrices[1]['qty']);
1232  $this->assertEquals($custGroup2, $tierPrices[1]['customer_group_id']);
1233 
1234  // update the product's tier prices: update 1st tier price, (delete the 2nd tier price), add a new one
1235  $custGroup3 = 1;
1236  $tierPrices[0]['value'] = 3.33;
1237  $tierPrices[0]['qty'] = 6;
1238  $tierPrices[1] = [
1239  'customer_group_id' => $custGroup3,
1240  'value' => 2.10,
1241  'qty' => 12,
1242  ];
1244  $response = $this->updateProduct($response);
1245 
1246  $this->assertArrayHasKey(self::KEY_TIER_PRICES, $response);
1248  $this->assertNotNull($tierPrices, "UPDATE 1: expected to have tier prices");
1249  $this->assertCount(2, $tierPrices, "UPDATE 1: expected to have 2 'tier_prices' objects");
1250  $this->assertEquals(3.33, $tierPrices[0]['value']);
1251  $this->assertEquals(6, $tierPrices[0]['qty']);
1252  $this->assertEquals($custGroup1, $tierPrices[0]['customer_group_id']);
1253  $this->assertEquals(2.10, $tierPrices[1]['value']);
1254  $this->assertEquals(12, $tierPrices[1]['qty']);
1255  $this->assertEquals($custGroup3, $tierPrices[1]['customer_group_id']);
1256 
1257  // update the product without any mention of tier prices; no change expected for tier pricing
1258  $response = $this->getProduct($productData[ProductInterface::SKU]);
1259  unset($response[self::KEY_TIER_PRICES]);
1260  $response = $this->updateProduct($response);
1261 
1262  $this->assertArrayHasKey(self::KEY_TIER_PRICES, $response);
1264  $this->assertNotNull($tierPrices, "UPDATE 2: expected to have tier prices");
1265  $this->assertCount(2, $tierPrices, "UPDATE 2: expected to have 2 'tier_prices' objects");
1266  $this->assertEquals(3.33, $tierPrices[0]['value']);
1267  $this->assertEquals(6, $tierPrices[0]['qty']);
1268  $this->assertEquals($custGroup1, $tierPrices[0]['customer_group_id']);
1269  $this->assertEquals(2.10, $tierPrices[1]['value']);
1270  $this->assertEquals(12, $tierPrices[1]['qty']);
1271  $this->assertEquals($custGroup3, $tierPrices[1]['customer_group_id']);
1272 
1273  // update the product with empty tier prices; expect to have the existing tier prices removed
1274  $response = $this->getProduct($productData[ProductInterface::SKU]);
1276  $response = $this->updateProduct($response);
1277  $this->assertArrayHasKey(self::KEY_TIER_PRICES, $response, "expected to have the 'tier_prices' key");
1278  $this->assertEmpty($response[self::KEY_TIER_PRICES], "expected to have an empty array of 'tier_prices'");
1279 
1280  // delete the product with tier prices; expect that all goes well
1281  $response = $this->deleteProduct($productData[ProductInterface::SKU]);
1282  $this->assertTrue($response);
1283  }
1284 
1288  private function getStockItemData()
1289  {
1290  return [
1292  StockItemInterface::QTY => 100500,
1314  ];
1315  }
1316 
1320  public function testProductCategoryLinks()
1321  {
1322  // Create simple product
1323  $productData = $this->getSimpleProductData();
1325  self::KEY_CATEGORY_LINKS => [['category_id' => 333, 'position' => 0]]
1326  ];
1327  $response = $this->saveProduct($productData);
1328  $this->assertEquals(
1329  [['category_id' => 333, 'position' => 0]],
1330  $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::KEY_CATEGORY_LINKS]
1331  );
1332  $response = $this->getProduct($productData[ProductInterface::SKU]);
1333  $this->assertArrayHasKey(ProductInterface::EXTENSION_ATTRIBUTES_KEY, $response);
1335  $this->assertArrayHasKey(self::KEY_CATEGORY_LINKS, $extensionAttributes);
1336  $this->assertEquals([['category_id' => 333, 'position' => 0]], $extensionAttributes[self::KEY_CATEGORY_LINKS]);
1337  }
1338 
1343  {
1344  $response = $this->getProduct('simple333');
1345  // update product without category_link or category_link is null
1347  $response = $this->updateProduct($response);
1348  $this->assertEquals(
1349  [['category_id' => 333, 'position' => 0]],
1350  $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::KEY_CATEGORY_LINKS]
1351  );
1352  unset($response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::KEY_CATEGORY_LINKS]);
1353  $response = $this->updateProduct($response);
1354  $this->assertEquals(
1355  [['category_id' => 333, 'position' => 0]],
1356  $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::KEY_CATEGORY_LINKS]
1357  );
1358  }
1359 
1364  {
1365  $response = $this->getProduct('simple333');
1366  // update category_link position
1368  ['category_id' => 333, 'position' => 10]
1369  ];
1370  $response = $this->updateProduct($response);
1371  $this->assertEquals(
1372  [['category_id' => 333, 'position' => 10]],
1373  $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::KEY_CATEGORY_LINKS]
1374  );
1375  }
1376 
1381  {
1382  $response = $this->getProduct('simple333');
1383  // unassign category_links from product
1385  $response = $this->updateProduct($response);
1386  $this->assertArrayNotHasKey(self::KEY_CATEGORY_LINKS, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]);
1387  }
1388 
1395  private function getMediaGalleryData($filename1, $encodedImage, $filename2)
1396  {
1397  return [
1398  [
1399  'position' => 1,
1400  'media_type' => 'image',
1401  'disabled' => true,
1402  'label' => 'tiny1',
1403  'types' => [],
1404  'content' => [
1405  'type' => 'image/jpeg',
1406  'name' => $filename1,
1407  'base64_encoded_data' => $encodedImage,
1408  ]
1409  ],
1410  [
1411  'position' => 2,
1412  'media_type' => 'image',
1413  'disabled' => false,
1414  'label' => 'tiny2',
1415  'types' => ['image', 'small_image'],
1416  'content' => [
1417  'type' => 'image/jpeg',
1418  'name' => $filename2,
1419  'base64_encoded_data' => $encodedImage,
1420  ]
1421  ],
1422  ];
1423  }
1424 
1425  public function testSpecialPrice()
1426  {
1427  $productData = $this->getSimpleProductData();
1428  $productData['custom_attributes'] = [
1429  ['attribute_code' => self::KEY_SPECIAL_PRICE, 'value' => '1']
1430  ];
1431  $this->saveProduct($productData);
1432  $response = $this->getProduct($productData[ProductInterface::SKU]);
1433  $customAttributes = $response['custom_attributes'];
1434  $this->assertNotEmpty($customAttributes);
1435  $missingAttributes = ['news_from_date', 'custom_design_from'];
1436  $expectedAttribute = ['special_price', 'special_from_date'];
1437  $attributeCodes = array_column($customAttributes, 'attribute_code');
1438  $this->assertEquals(0, count(array_intersect($attributeCodes, $missingAttributes)));
1439  $this->assertEquals(2, count(array_intersect($attributeCodes, $expectedAttribute)));
1440  }
1441 
1450  public function testResetSpecialPrice()
1451  {
1452  $this->_markTestAsRestOnly(
1453  'In order to properly run this test for SOAP, XML must be used to specify <value></value> ' .
1454  'for the special_price value. Otherwise, the null value gets processed as a string and ' .
1455  'cast to a double value of 0.0.'
1456  );
1457  $productData = $this->getSimpleProductData();
1458  $productData['custom_attributes'] = [
1459  ['attribute_code' => self::KEY_SPECIAL_PRICE, 'value' => 5.00]
1460  ];
1461  $this->saveProduct($productData);
1462  $response = $this->getProduct($productData[ProductInterface::SKU]);
1463  $customAttributes = array_column($response['custom_attributes'], 'value', 'attribute_code');
1464  $this->assertEquals(5, $customAttributes[self::KEY_SPECIAL_PRICE]);
1465  $productData['custom_attributes'] = [
1466  ['attribute_code' => self::KEY_SPECIAL_PRICE, 'value' => null]
1467  ];
1468  $this->saveProduct($productData);
1469  $response = $this->getProduct($productData[ProductInterface::SKU]);
1470  $customAttributes = array_column($response['custom_attributes'], 'value', 'attribute_code');
1471  $this->assertFalse(array_key_exists(self::KEY_SPECIAL_PRICE, $customAttributes));
1472  }
1473 
1474  public function testUpdateStatus()
1475  {
1476  // Create simple product
1477  $productData = [
1478  ProductInterface::SKU => "product_simple_502",
1479  ProductInterface::NAME => "Product Simple 502",
1481  ProductInterface::TYPE_ID => 'simple',
1482  ProductInterface::PRICE => 100,
1484  ProductInterface::TYPE_ID => 'simple',
1486  ];
1487 
1488  // Save product with status disabled
1489  $this->saveProduct($productData);
1490  $response = $this->getProduct($productData[ProductInterface::SKU]);
1491  $this->assertEquals(0, $response['status']);
1492 
1493  // Update the product
1494  $productData[ProductInterface::PRICE] = 200;
1495  $this->saveProduct($productData);
1496  $response = $this->getProduct($productData[ProductInterface::SKU]);
1497 
1498  // Status should still be disabled
1499  $this->assertEquals(0, $response['status']);
1500  // Price should be updated
1501  $this->assertEquals(200, $response['price']);
1502  }
1503 
1516  {
1517  $multiselectAttributeCode = 'multiselect_attribute';
1518  $multiselectOptions = $this->getAttributeOptions($multiselectAttributeCode);
1519  $option1 = $multiselectOptions[1]['value'];
1520  $option2 = $multiselectOptions[2]['value'];
1521 
1522  $productData = $this->getSimpleProductData();
1523  $productData['custom_attributes'] = [
1524  ['attribute_code' => $multiselectAttributeCode, 'value' => "{$option1},{$option2}"]
1525  ];
1526  $this->saveProduct($productData, 'all');
1527 
1528  $this->assertMultiselectValue(
1529  $productData[ProductInterface::SKU],
1530  $multiselectAttributeCode,
1531  "{$option1},{$option2}"
1532  );
1533 
1534  $productData['custom_attributes'] = [
1535  ['attribute_code' => $multiselectAttributeCode, 'value' => ""]
1536  ];
1537  $this->saveProduct($productData, 'all');
1538  $this->assertMultiselectValue(
1539  $productData[ProductInterface::SKU],
1540  $multiselectAttributeCode,
1541  ""
1542  );
1543  }
1544 
1549  private function getAttributeOptions($attributeCode)
1550  {
1551  $serviceInfo = [
1552  'rest' => [
1553  'resourcePath' => '/V1/products/attributes/' . $attributeCode . '/options',
1555  ],
1556  'soap' => [
1557  'service' => 'catalogProductAttributeOptionManagementV1',
1558  'serviceVersion' => 'V1',
1559  'operation' => 'catalogProductAttributeOptionManagementV1getItems',
1560  ],
1561  ];
1562 
1563  return $this->_webApiCall($serviceInfo, ['attributeCode' => $attributeCode]);
1564  }
1565 
1571  private function assertMultiselectValue($productSku, $multiselectAttributeCode, $expectedMultiselectValue)
1572  {
1573  $response = $this->getProduct($productSku, 'all');
1574  $customAttributes = $response['custom_attributes'];
1575  $this->assertNotEmpty($customAttributes);
1576  $multiselectValue = null;
1577  foreach ($customAttributes as $customAttribute) {
1578  if ($customAttribute['attribute_code'] == $multiselectAttributeCode) {
1579  $multiselectValue = $customAttribute['value'];
1580  break;
1581  }
1582  }
1583  $this->assertEquals($expectedMultiselectValue, $multiselectValue);
1584  }
1585 }
$response
Definition: 404.php:11
_webApiCall( $serviceInfo, $arguments=[], $webApiAdapterCode=null, $storeCode=null, $integration=null)
$id
Definition: fieldset.phtml:14
defined('TESTS_BP')||define('TESTS_BP' __DIR__
Definition: _bootstrap.php:60
$storeManager
testGetListWithFilteringByStore(array $searchCriteria, array $skus, $expectedProductCount=null)
foreach($websiteCodes as $websiteCode) $skus
$searchCriteria
$storeCode
Definition: indexer.php:15
$attributeCode
Definition: extend.phtml:12
$extensionAttributes
Definition: payment.php:22
$searchCriteriaBuilder
if(!isset($_GET['website_code'])) $websiteCode
Definition: website.php:11
$i
Definition: gallery.phtml:31
$index
Definition: list.phtml:44
$code
Definition: info.phtml:12