1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26
27declare(strict_types=1);
28
29namespace PrestaShop\PrestaShop\Adapter\Product\QueryHandler;
30
31use Customization;
32use DateTime;
33use PrestaShop\PrestaShop\Adapter\Product\Repository\ProductRepository;
34use PrestaShop\PrestaShop\Adapter\Product\Stock\Repository\StockAvailableRepository;
35use PrestaShop\PrestaShop\Adapter\Product\VirtualProduct\Repository\VirtualProductFileRepository;
36use PrestaShop\PrestaShop\Adapter\Tax\TaxComputer;
37use PrestaShop\PrestaShop\Core\Domain\Country\ValueObject\CountryId;
38use PrestaShop\PrestaShop\Core\Domain\Product\ProductCustomizabilitySettings;
39use PrestaShop\PrestaShop\Core\Domain\Product\Query\GetProductForEditing;
40use PrestaShop\PrestaShop\Core\Domain\Product\QueryHandler\GetProductForEditingHandlerInterface;
41use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\LocalizedTags;
42use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductBasicInformation;
43use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductCategoriesInformation;
44use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductCustomizationOptions;
45use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductDetails;
46use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductForEditing;
47use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductOptions;
48use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductPricesInformation;
49use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductSeoOptions;
50use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductShippingInformation;
51use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductStockInformation;
52use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;
53use PrestaShop\PrestaShop\Core\Domain\Product\VirtualProductFile\Exception\VirtualProductFileNotFoundException;
54use PrestaShop\PrestaShop\Core\Domain\Product\VirtualProductFile\QueryResult\VirtualProductFileForEditing;
55use PrestaShop\PrestaShop\Core\Domain\TaxRulesGroup\ValueObject\TaxRulesGroupId;
56use PrestaShop\PrestaShop\Core\Util\DateTime\DateTime as DateTimeUtil;
57use PrestaShop\PrestaShop\Core\Util\Number\NumberExtractor;
58use PrestaShop\PrestaShop\Core\Util\Number\NumberExtractorException;
59use Product;
60use Tag;
61
62/**
63 * Handles the query GetEditableProduct using legacy ObjectModel
64 */
65final class GetProductForEditingHandler implements GetProductForEditingHandlerInterface
66{
67    /**
68     * @var NumberExtractor
69     */
70    private $numberExtractor;
71
72    /**
73     * @var ProductRepository
74     */
75    private $productRepository;
76
77    /**
78     * @var StockAvailableRepository
79     */
80    private $stockAvailableRepository;
81
82    /**
83     * @var VirtualProductFileRepository
84     */
85    private $virtualProductFileRepository;
86
87    /**
88     * @var TaxComputer
89     */
90    private $taxComputer;
91
92    /**
93     * @var int
94     */
95    private $countryId;
96
97    /**
98     * @param NumberExtractor $numberExtractor
99     * @param ProductRepository $productRepository
100     * @param StockAvailableRepository $stockAvailableRepository
101     * @param VirtualProductFileRepository $virtualProductFileRepository
102     * @param TaxComputer $taxComputer
103     * @param int $countryId
104     */
105    public function __construct(
106        NumberExtractor $numberExtractor,
107        ProductRepository $productRepository,
108        StockAvailableRepository $stockAvailableRepository,
109        VirtualProductFileRepository $virtualProductFileRepository,
110        TaxComputer $taxComputer,
111        int $countryId
112    ) {
113        $this->numberExtractor = $numberExtractor;
114        $this->stockAvailableRepository = $stockAvailableRepository;
115        $this->virtualProductFileRepository = $virtualProductFileRepository;
116        $this->taxComputer = $taxComputer;
117        $this->countryId = $countryId;
118        $this->productRepository = $productRepository;
119    }
120
121    /**
122     * {@inheritdoc}
123     */
124    public function handle(GetProductForEditing $query): ProductForEditing
125    {
126        $product = $this->productRepository->get($query->getProductId());
127
128        return new ProductForEditing(
129            (int) $product->id,
130            $product->getProductType(),
131            $this->getCustomizationOptions($product),
132            $this->getBasicInformation($product),
133            $this->getCategoriesInformation($product),
134            $this->getPricesInformation($product),
135            $this->getOptions($product),
136            $this->getDetails($product),
137            $this->getShippingInformation($product),
138            $this->getSeoOptions($product),
139            $product->getAssociatedAttachmentIds(),
140            $this->getProductStockInformation($product),
141            $this->getVirtualProductFile($product)
142        );
143    }
144
145    /**
146     * @param Product $product
147     *
148     * @return ProductBasicInformation
149     */
150    private function getBasicInformation(Product $product): ProductBasicInformation
151    {
152        return new ProductBasicInformation(
153            $product->name,
154            $product->description,
155            $product->description_short,
156            $this->getLocalizedTagsList((int) $product->id)
157        );
158    }
159
160    /**
161     * @param Product $product
162     *
163     * @return ProductCategoriesInformation
164     */
165    private function getCategoriesInformation(Product $product): ProductCategoriesInformation
166    {
167        $categoryIds = array_map('intval', $product->getCategories());
168        $defaultCategoryId = (int) $product->id_category_default;
169
170        return new ProductCategoriesInformation($categoryIds, $defaultCategoryId);
171    }
172
173    /**
174     * @param Product $product
175     *
176     * @return ProductPricesInformation
177     */
178    private function getPricesInformation(Product $product): ProductPricesInformation
179    {
180        $priceTaxExcluded = $this->numberExtractor->extract($product, 'price');
181        $priceTaxIncluded = $this->taxComputer->computePriceWithTaxes(
182            $priceTaxExcluded,
183            new TaxRulesGroupId((int) $product->id_tax_rules_group),
184            new CountryId($this->countryId)
185        );
186
187        return new ProductPricesInformation(
188            $priceTaxExcluded,
189            $priceTaxIncluded,
190            $this->numberExtractor->extract($product, 'ecotax'),
191            (int) $product->id_tax_rules_group,
192            (bool) $product->on_sale,
193            $this->numberExtractor->extract($product, 'wholesale_price'),
194            $this->numberExtractor->extract($product, 'unit_price'),
195            (string) $product->unity,
196            $this->numberExtractor->extract($product, 'unit_price_ratio')
197        );
198    }
199
200    /**
201     * @param Product $product
202     *
203     * @return ProductOptions
204     */
205    private function getOptions(Product $product): ProductOptions
206    {
207        return new ProductOptions(
208            (bool) $product->active,
209            $product->visibility,
210            (bool) $product->available_for_order,
211            (bool) $product->online_only,
212            (bool) $product->show_price,
213            $product->condition,
214            (bool) $product->show_condition,
215            (int) $product->id_manufacturer
216        );
217    }
218
219    /**
220     * @param Product $product
221     *
222     * @return ProductDetails
223     */
224    private function getDetails(Product $product): ProductDetails
225    {
226        return new ProductDetails(
227            $product->isbn,
228            $product->upc,
229            $product->ean13,
230            $product->mpn,
231            $product->reference
232        );
233    }
234
235    /**
236     * @param Product $product
237     *
238     * @return ProductShippingInformation
239     *
240     * @throws NumberExtractorException
241     */
242    private function getShippingInformation(Product $product): ProductShippingInformation
243    {
244        $carrierReferences = array_map(function ($carrier): int {
245            return (int) $carrier['id_reference'];
246        }, $product->getCarriers());
247
248        return new ProductShippingInformation(
249            $this->numberExtractor->extract($product, 'width'),
250            $this->numberExtractor->extract($product, 'height'),
251            $this->numberExtractor->extract($product, 'depth'),
252            $this->numberExtractor->extract($product, 'weight'),
253            $this->numberExtractor->extract($product, 'additional_shipping_cost'),
254            $carrierReferences,
255            (int) $product->additional_delivery_times,
256            $product->delivery_in_stock,
257            $product->delivery_out_stock
258        );
259    }
260
261    /**
262     * @param int $productId
263     *
264     * @return LocalizedTags[]
265     */
266    private function getLocalizedTagsList(int $productId): array
267    {
268        $tags = Tag::getProductTags($productId);
269
270        if (!$tags) {
271            return [];
272        }
273
274        $localizedTagsList = [];
275
276        foreach ($tags as $langId => $localizedTags) {
277            $localizedTagsList[] = new LocalizedTags((int) $langId, $localizedTags);
278        }
279
280        return $localizedTagsList;
281    }
282
283    /**
284     * @param Product $product
285     *
286     * @return ProductCustomizationOptions
287     */
288    private function getCustomizationOptions(Product $product): ProductCustomizationOptions
289    {
290        if (!Customization::isFeatureActive()) {
291            return ProductCustomizationOptions::createNotCustomizable();
292        }
293
294        $textFieldsCount = (int) $product->text_fields;
295        $fileFieldsCount = (int) $product->uploadable_files;
296
297        switch ((int) $product->customizable) {
298            case ProductCustomizabilitySettings::ALLOWS_CUSTOMIZATION:
299                $options = ProductCustomizationOptions::createAllowsCustomization($textFieldsCount, $fileFieldsCount);
300                break;
301            case ProductCustomizabilitySettings::REQUIRES_CUSTOMIZATION:
302                $options = ProductCustomizationOptions::createRequiresCustomization($textFieldsCount, $fileFieldsCount);
303                break;
304            default:
305                $options = ProductCustomizationOptions::createNotCustomizable();
306        }
307
308        return $options;
309    }
310
311    /**
312     * @param Product $product
313     *
314     * @return ProductSeoOptions
315     */
316    private function getSeoOptions(Product $product): ProductSeoOptions
317    {
318        return new ProductSeoOptions(
319            $product->meta_title,
320            $product->meta_description,
321            $product->link_rewrite,
322            $product->redirect_type,
323            (int) $product->id_type_redirected
324        );
325    }
326
327    /**
328     * Returns the product stock infos, it's important that the Product is fetched with stock data
329     *
330     * @param Product $product
331     *
332     * @return ProductStockInformation
333     */
334    private function getProductStockInformation(Product $product): ProductStockInformation
335    {
336        //@todo: In theory StockAvailable is created for each product when Product::add is called,
337        //  but we should explore some multishop edgecases
338        //  (like shop ids might be missing and foreach loop won't start resulting in a missing StockAvailable for product)
339        $stockAvailable = $this->stockAvailableRepository->getForProduct(new ProductId($product->id));
340
341        return new ProductStockInformation(
342            (int) $product->pack_stock_type,
343            (int) $stockAvailable->out_of_stock,
344            (int) $stockAvailable->quantity,
345            (int) $product->minimal_quantity,
346            (int) $product->low_stock_threshold,
347            (bool) $product->low_stock_alert,
348            $product->available_now,
349            $product->available_later,
350            $stockAvailable->location,
351            DateTimeUtil::NULL_DATE === $product->available_date ? null : new DateTime($product->available_date)
352        );
353    }
354
355    /**
356     * Get virtual product file
357     * Legacy object ProductDownload is referred as VirtualProductFile in Core
358     *
359     * @param Product $product
360     *
361     * @return VirtualProductFileForEditing|null
362     */
363    private function getVirtualProductFile(Product $product): ?VirtualProductFileForEditing
364    {
365        try {
366            $virtualProductFile = $this->virtualProductFileRepository->findByProductId(new ProductId($product->id));
367        } catch (VirtualProductFileNotFoundException $e) {
368            return null;
369        }
370
371        return new VirtualProductFileForEditing(
372            (int) $virtualProductFile->id,
373            $virtualProductFile->filename,
374            $virtualProductFile->display_filename,
375            (int) $virtualProductFile->nb_days_accessible,
376            (int) $virtualProductFile->nb_downloadable,
377            $virtualProductFile->date_expiration === DateTimeUtil::NULL_DATETIME ? null : new DateTime($virtualProductFile->date_expiration)
378        );
379    }
380}
381