Completed
Pull Request — master (#392)
by Jonas
03:13
created

ProductToShop::addProductToStream()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 2
1
<?php
2
/**
3
 * (c) shopware AG <[email protected]>
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace ShopwarePlugins\Connect\Components;
9
10
use Shopware\Bundle\SearchBundle\Sorting\ReleaseDateSorting;
11
use Shopware\Connect\Gateway;
12
use Shopware\Components\Model\CategoryDenormalization;
13
use Shopware\Connect\ProductToShop as ProductToShopBase;
14
use Shopware\Connect\Struct\OrderStatus;
15
use Shopware\Connect\Struct\Product;
16
use Shopware\Models\Article\Article as ProductModel;
17
use Shopware\Models\Order\Order;
18
use Shopware\Models\Category\Category;
19
use Shopware\Models\Order\Status;
20
use Shopware\Models\Article\Detail as DetailModel;
21
use Shopware\Models\Attribute\Article as AttributeModel;
22
use Shopware\Components\Model\ModelManager;
23
use Shopware\Connect\Struct\PriceRange;
24
use Shopware\Connect\Struct\ProductUpdate;
25
use Shopware\CustomModels\Connect\ProductStreamAttribute;
26
use Shopware\Models\Customer\Group;
27
use Shopware\Connect\Struct\Property;
28
use Shopware\Models\ProductStream\ProductStream;
29
use Shopware\Models\Property\Group as PropertyGroup;
30
use Shopware\Models\Property\Option as PropertyOption;
31
use Shopware\Models\Property\Value as PropertyValue;
32
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamRepository;
33
use ShopwarePlugins\Connect\Components\ProductStream\ProductStreamService;
34
use ShopwarePlugins\Connect\Components\Translations\LocaleMapper;
35
use ShopwarePlugins\Connect\Components\Gateway\ProductTranslationsGateway;
36
use ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway;
37
use ShopwarePlugins\Connect\Components\Utils\UnitMapper;
38
use Shopware\CustomModels\Connect\Attribute as ConnectAttribute;
39
use Shopware\Models\Article\Image;
40
use Shopware\Models\Article\Supplier;
41
use Shopware\Models\Tax\Tax;
42
43
/**
44
 * The interface for products imported *from* connect *to* the local shop
45
 *
46
 * @category  Shopware
47
 * @package   Shopware\Plugins\SwagConnect
48
 */
49
class ProductToShop implements ProductToShopBase
50
{
51
    /**
52
     * @var Helper
53
     */
54
    private $helper;
55
56
    /**
57
     * @var ModelManager
58
     */
59
    private $manager;
60
61
    /**
62
     * @var \ShopwarePlugins\Connect\Components\Config
63
     */
64
    private $config;
65
66
    /**
67
     * @var ImageImport
68
     */
69
    private $imageImport;
70
71
    /**
72
     * @var \ShopwarePlugins\Connect\Components\VariantConfigurator
73
     */
74
    private $variantConfigurator;
75
76
    /**
77
     * @var MarketplaceGateway
78
     */
79
    private $marketplaceGateway;
80
81
    /**
82
     * @var ProductTranslationsGateway
83
     */
84
    private $productTranslationsGateway;
85
86
    /**
87
     * @var \Shopware\Models\Shop\Repository
88
     */
89
    private $shopRepository;
90
91
    private $localeRepository;
92
93
    /**
94
     * @var CategoryResolver
95
     */
96
    private $categoryResolver;
97
98
    /**
99
     * @var \Shopware\Connect\Gateway
100
     */
101
    private $connectGateway;
102
103
    /**
104
     * @var \Enlight_Event_EventManager
105
     */
106
    private $eventManager;
107
108
    /**
109
     * @var CategoryDenormalization
110
     */
111
    private $categoryDenormalization;
112
113
    /**
114
     * @param Helper $helper
115
     * @param ModelManager $manager
116
     * @param ImageImport $imageImport
117
     * @param \ShopwarePlugins\Connect\Components\Config $config
118
     * @param VariantConfigurator $variantConfigurator
119
     * @param \ShopwarePlugins\Connect\Components\Marketplace\MarketplaceGateway $marketplaceGateway
120
     * @param ProductTranslationsGateway $productTranslationsGateway
121
     * @param CategoryResolver $categoryResolver
122
     * @param Gateway $connectGateway
123
     * @param \Enlight_Event_EventManager $eventManager
124
     * @param CategoryDenormalization $categoryDenormalization
125
     */
126
    public function __construct(
127
        Helper $helper,
128
        ModelManager $manager,
129
        ImageImport $imageImport,
130
        Config $config,
131
        VariantConfigurator $variantConfigurator,
132
        MarketplaceGateway $marketplaceGateway,
133
        ProductTranslationsGateway $productTranslationsGateway,
134
        CategoryResolver $categoryResolver,
135
        Gateway $connectGateway,
136
        \Enlight_Event_EventManager $eventManager,
137
        CategoryDenormalization $categoryDenormalization
138
    ) {
139
        $this->helper = $helper;
140
        $this->manager = $manager;
141
        $this->config = $config;
142
        $this->imageImport = $imageImport;
143
        $this->variantConfigurator = $variantConfigurator;
144
        $this->marketplaceGateway = $marketplaceGateway;
145
        $this->productTranslationsGateway = $productTranslationsGateway;
146
        $this->categoryResolver = $categoryResolver;
147
        $this->connectGateway = $connectGateway;
148
        $this->eventManager = $eventManager;
149
        $this->categoryDenormalization = $categoryDenormalization;
150
    }
151
152
    /**
153
     * Start transaction
154
     *
155
     * Starts a transaction, which includes all insertOrUpdate and delete
156
     * operations, as well as the revision updates.
157
     *
158
     * @return void
159
     */
160
    public function startTransaction()
161
    {
162
        $this->manager->getConnection()->beginTransaction();
163
    }
164
165
    /**
166
     * Commit transaction
167
     *
168
     * Commits the transactions, once all operations are queued.
169
     *
170
     * @return void
171
     */
172
    public function commit()
173
    {
174
        $this->manager->getConnection()->commit();
175
    }
176
177
    /**
178
     * Import or update given product
179
     *
180
     * Store product in your shop database as an external product. The
181
     * associated sourceId
182
     *
183
     * @param Product $product
184
     */
185
    public function insertOrUpdate(Product $product)
186
    {
187
        /** @var Product $product */
188
        $product = $this->eventManager->filter(
189
            'Connect_ProductToShop_InsertOrUpdate_Before',
190
            $product
191
        );
192
193
        // todo@dn: Set dummy values and make product inactive
194
        if (empty($product->title) || empty($product->vendor)) {
195
            return;
196
        }
197
198
        $number = $this->generateSKU($product);
199
200
        $detail = $this->helper->getArticleDetailModelByProduct($product);
201
        $detail = $this->eventManager->filter(
202
            'Connect_Merchant_Get_Article_Detail_After',
203
            $detail,
204
            [
205
                'product' => $product,
206
                'subject' => $this
207
            ]
208
        );
209
210
        $isMainVariant = false;
211
        if ($detail === null) {
212
            $active = $this->config->getConfig('activateProductsAutomatically', false) ? true : false;
213
214
            $model = $this->getSWProductModel($product, $active, $isMainVariant);
215
216
            $detail = $this->generateNewDetail($product, $model);
217
        } else {
218
            /** @var ProductModel $model */
219
            $model = $detail->getArticle();
220
            // fix for isMainVariant flag
221
            // in connect attribute table
222
            $mainDetail = $model->getMainDetail();
223
            $isMainVariant = $this->checkIfMainVariant($detail, $mainDetail);
224
225
            $this->updateConfiguratorSetTypeFromProduct($model, $product);
226
227
            $this->cleanUpConfiguratorSet($model, $product);
228
        }
229
230
        $detail->setNumber($number);
231
232
        $this->removeConnectImportedCategories($model);
233
234
        $detailAttribute = $this->getOrCreateAttributeModel($detail, $model);
235
236
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
237
        // configure main variant and groupId
238
        if ($isMainVariant === true) {
239
            $connectAttribute->setIsMainVariant(true);
240
        }
241
        $connectAttribute->setGroupId($product->groupId);
242
243
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
244
        $this->setPropertiesForNewProducts($updateFields, $model, $detailAttribute, $product);
245
246
        $this->saveVat($product, $model);
247
248
        $this->applyProductProperties($model, $product);
249
250
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
251
252
        $this->setConnectAttributesFromProduct($connectAttribute, $product);
253
254
        // store product categories to connect attribute
255
        $connectAttribute->setCategory($product->categories);
256
257
        $connectAttribute->setLastUpdateFlag($flag);
258
259
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
260
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
261
262
        $this->updateDetailFromProduct($detail, $product);
263
264
        // some shops have feature "sell not in stock",
265
        // then end customer should be able to by the product with stock = 0
266
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
267
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
268
            $model->setLastStock(false);
269
        } else {
270
            $model->setLastStock(true);
271
        }
272
273
        $this->detailSetUnit($detail, $product, $detailAttribute);
274
275
        $this->detailSetAttributes($detail, $product);
276
277
        $this->connectAttributeSetLastUpdate($connectAttribute, $product);
278
279
        if ($model->getMainDetail() === null) {
280
            $model->setMainDetail($detail);
281
        }
282
283
        if ($detail->getAttribute() === null) {
284
            $detail->setAttribute($detailAttribute);
285
            $detailAttribute->setArticle($model);
286
        }
287
288
        $connectAttribute->setArticle($model);
289
        $connectAttribute->setArticleDetail($detail);
290
291
        $this->eventManager->notify(
292
            'Connect_Merchant_Saving_ArticleAttribute_Before',
293
            [
294
                'subject' => $this,
295
                'connectAttribute' => $connectAttribute
296
            ]
297
        );
298
299
        //article has to be flushed
300
        $this->manager->persist($model);
301
        $this->manager->persist($connectAttribute);
302
        $this->manager->persist($detail);
303
        $this->manager->flush();
304
305
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId(), $product->shopId);
306
        $categories = $this->categoryResolver->resolve($product->categories, $product->shopId);
307
        if (count($categories) > 0) {
308
            $detailAttribute->setConnectMappedCategory(true);
309
        }
310
311
        $this->manager->persist($detailAttribute);
312
        $this->manager->flush();
313
314
        $this->categoryDenormalization($model, $categories);
315
316
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
317
        // Only set prices, if fixedPrice is active or price updates are configured
318
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
319
            $this->setPrice($model, $detail, $product);
320
        }
321
        // If the price is not being update, update the purchasePrice anyway
322
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
323
324
        $this->manager->clear();
325
326
        $this->addArticleTranslations($model, $product);
327
328
        //clear cache for that article
329
        $this->helper->clearArticleCache($model->getId());
330
331
        if ($updateFields['image']) {
332
            // Reload the model in order to not to work an the already flushed model
333
            $model = $this->helper->getArticleModelByProduct($product);
334
            // import only global images for article
335
            $this->imageImport->importImagesForArticle(array_diff($product->images, $product->variantImages), $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by $this->helper->getArticleModelByProduct($product) on line 333 can be null; however, ShopwarePlugins\Connect\...mportImagesForArticle() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
336
            // Reload the article detail model in order to not to work an the already flushed model
337
            $detail = $this->helper->getArticleDetailModelByProduct($product);
338
            // import only specific images for variant
339
            $this->imageImport->importImagesForDetail($product->variantImages, $detail);
0 ignored issues
show
Bug introduced by
It seems like $detail defined by $this->helper->getArticl...odelByProduct($product) on line 337 can be null; however, ShopwarePlugins\Connect\...importImagesForDetail() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
340
        }
341
342
        $this->eventManager->notify(
343
            'Connect_ProductToShop_InsertOrUpdate_After',
344
            [
345
                'connectProduct' => $product,
346
                'shopArticleDetail' => $detail
347
            ]
348
        );
349
350
        $stream = $this->getOrCreateStream($product);
351
        $this->addProductToStream($stream, $model);
0 ignored issues
show
Bug introduced by
It seems like $model can be null; however, addProductToStream() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
352
    }
353
354
    /**
355
     * @param Product $product
356
     * @return string
357
     */
358
    private function generateSKU(Product $product)
359
    {
360
        if (!empty($product->sku)) {
361
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
362
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
363
            if ($duplicatedDetail
364
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
365
            ) {
366
                $this->deleteDetail($duplicatedDetail);
367
            }
368
        } else {
369
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
370
        }
371
372
        return $number;
373
    }
374
375
    /**
376
     * @param DetailModel $detailModel
377
     */
378
    private function deleteDetail(DetailModel $detailModel)
379
    {
380
        $this->eventManager->notify(
381
            'Connect_Merchant_Delete_Product_Before',
382
            [
383
                'subject' => $this,
384
                'articleDetail' => $detailModel
385
            ]
386
        );
387
388
        $article = $detailModel->getArticle();
389
        // Not sure why, but the Attribute can be NULL
390
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
391
        $this->manager->remove($detailModel);
392
393
        if ($attribute) {
394
            $this->manager->remove($attribute);
395
        }
396
397
        // if removed variant is main variant
398
        // find first variant which is not main and mark it
399
        if ($detailModel->getKind() === 1) {
400
            /** @var \Shopware\Models\Article\Detail $variant */
401
            foreach ($article->getDetails() as $variant) {
402
                if ($variant->getId() != $detailModel->getId()) {
403
                    $variant->setKind(1);
404
                    $article->setMainDetail($variant);
405
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
406
                    if (!$connectAttribute) {
407
                        continue;
408
                    }
409
                    $connectAttribute->setIsMainVariant(true);
410
                    $this->manager->persist($connectAttribute);
411
                    $this->manager->persist($article);
412
                    $this->manager->persist($variant);
413
                    break;
414
                }
415
            }
416
        }
417
418
        if (count($details = $article->getDetails()) === 1) {
419
            $details->clear();
420
            $this->manager->remove($article);
421
        }
422
      
423
        //save category Ids before flush
424
        $oldCategoryIds = array_map(function ($category) {
425
            return $category->getId();
426
        }, $article->getCategories()->toArray());
427
428
        // Do not remove flush. It's needed when remove article,
429
        // because duplication of ordernumber. Even with remove before
430
        // persist calls mysql throws exception "Duplicate entry"
431
        $this->manager->flush();
432
        // always clear entity manager, because $article->getDetails() returns
433
        // more than 1 detail, but all of them were removed except main one.
434
        $this->manager->clear();
435
436
        // call this after flush because article has to be deleted that this works
437
        if (count($oldCategoryIds) > 0) {
438
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
439
        }
440
    }
441
442
    /**
443
     * @param Product $product
444
     * @param $active
445
     * @param $isMainVariant
446
     * @return null|Article
447
     */
448
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
449
    {
450
        if ($product->groupId !== null) {
451
            $model = $this->helper->getArticleByRemoteProduct($product);
452
            if (!$model instanceof \Shopware\Models\Article\Article) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Article does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
453
                $model = $this->helper->createProductModel($product);
454
                $model->setActive($active);
455
                $isMainVariant = true;
456
            }
457
        } else {
458
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
459
            if (!$model instanceof \Shopware\Models\Article\Article) {
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Article\Article does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
460
                $model = $this->helper->createProductModel($product);
461
                $model->setActive($active);
462
            }
463
        }
464
465
        return $model;
466
    }
467
468
    /**
469
     * @param Product $product
470
     * @param $model
471
     * @return DetailModel
472
     */
473
    private function generateNewDetail(Product $product, $model)
474
    {
475
        $detail = new DetailModel();
476
        $detail->setActive($model->getActive());
477
        $this->manager->persist($detail);
478
        $detail->setArticle($model);
479
        $model->getDetails()->add($detail);
480
        if (!empty($product->variant)) {
481
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
482
        }
483
484
        return $detail;
485
    }
486
487
    /**
488
     * @param DetailModel $detail
489
     * @param DetailModel $mainDetail
490
     * @return bool
491
     */
492
    private function checkIfMainVariant(DetailModel $detail, DetailModel $mainDetail)
493
    {
494
        return $detail->getId() === $mainDetail->getId();
495
    }
496
497
    /**
498
     * @param ProductModel $model
499
     * @param Product $product
500
     */
501
    private function updateConfiguratorSetTypeFromProduct(ProductModel $model, Product $product)
502
    {
503
        if (!empty($product->variant)) {
504
            $configSet = $model->getConfiguratorSet();
505
            $configSet->setType($product->configuratorSetType);
506
        }
507
    }
508
509
    /**
510
     * @param ProductModel $model
511
     * @param Product $product
512
     */
513
    private function cleanUpConfiguratorSet(ProductModel $model, Product $product)
514
    {
515
        if (empty($product->variant) && $model->getConfiguratorSet()) {
516
            $this->manager->getConnection()->executeQuery(
517
                'UPDATE s_articles SET configurator_set_id = NULL WHERE id = ?',
518
                [$model->getId()]
519
            );
520
        }
521
    }
522
523
    /**
524
     * @param ProductModel $model
525
     */
526
    private function removeConnectImportedCategories(ProductModel $model)
527
    {
528
        /** @var \Shopware\Models\Category\Category $category */
529
        foreach ($model->getCategories() as $category) {
530
            $attribute = $category->getAttribute();
531
            if (!$attribute) {
532
                continue;
533
            }
534
535
            if ($attribute->getConnectImported()) {
536
                $model->removeCategory($category);
537
            }
538
        }
539
    }
540
541
    /**
542
     * @param DetailModel $detail
543
     * @param ProductModel $model
544
     * @return AttributeModel
545
     */
546
    private function getOrCreateAttributeModel(DetailModel $detail, ProductModel $model)
547
    {
548
        $detailAttribute = $detail->getAttribute();
549
        if (!$detailAttribute) {
550
            $detailAttribute = new AttributeModel();
551
            $detail->setAttribute($detailAttribute);
552
            $model->setAttribute($detailAttribute);
553
            $detailAttribute->setArticle($model);
554
            $detailAttribute->setArticleDetail($detail);
555
        }
556
557
        return $detailAttribute;
558
    }
559
560
    /**
561
     * Get array of update info for the known fields
562
     *
563
     * @param $model
564
     * @param $detail
565
     * @param $attribute
566
     * @param $product
567
     * @return array
568
     */
569
    public function getUpdateFields($model, $detail, $attribute, $product)
570
    {
571
        // This also defines the flags of these fields
572
        $fields = $this->helper->getUpdateFlags();
573
        $flagsByName = array_flip($fields);
574
575
        $flag = 0;
576
        $output = [];
577
        foreach ($fields as $key => $field) {
578
            // Don't handle the imageInitialImport flag
579
            if ($field == 'imageInitialImport') {
580
                continue;
581
            }
582
583
            // If this is a new product
584
            if (!$model->getId() && $field == 'image' && !$this->config->getConfig('importImagesOnFirstImport',
585
                    false)) {
586
                $output[$field] = false;
587
                $flag |= $flagsByName['imageInitialImport'];
588
                continue;
589
            }
590
591
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
592
            $output[$field] = $updateAllowed;
593
            if (!$updateAllowed && $this->hasFieldChanged($field, $model, $detail, $product)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $updateAllowed of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
594
                $flag |= $key;
595
            }
596
        }
597
598
        return [$output, $flag];
599
    }
600
601
    /**
602
     * Helper method to determine if a given $fields may/must be updated.
603
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
604
     * this method cannot be used after the model in question was already flushed.
605
     *
606
     * @param $field
607
     * @param $model ProductModel
608
     * @param $attribute ConnectAttribute
609
     * @throws \RuntimeException
610
     * @return bool|null
611
     */
612
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
613
    {
614
        $allowed = [
615
            'ShortDescription',
616
            'LongDescription',
617
            'AdditionalDescription',
618
            'Image',
619
            'Price',
620
            'Name',
621
        ];
622
623
        // Always allow updates for new models
624
        if (!$model->getId()) {
625
            return true;
626
        }
627
628
        $field = ucfirst($field);
629
        $attributeGetter = 'getUpdate' . $field;
630
        $configName = 'overwriteProduct' . $field;
631
632
        if (!in_array($field, $allowed)) {
633
            throw new \RuntimeException("Unknown update field {$field}");
634
        }
635
636
        $attributeValue = $attribute->$attributeGetter();
637
638
639
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
640
        // Once we have a supplier based configuration, we need to take it into account here
641
        if ($attributeValue == null || $attributeValue == 'inherit') {
642
            return $this->config->getConfig($configName, true);
643
        }
644
645
        return $attributeValue == 'overwrite';
646
    }
647
648
    /**
649
     * Determine if a given field has changed
650
     *
651
     * @param $field
652
     * @param ProductModel $model
653
     * @param DetailModel $detail
654
     * @param Product $product
655
     * @return bool
656
     */
657
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
658
    {
659
        switch ($field) {
660
            case 'shortDescription':
661
                return $model->getDescription() != $product->shortDescription;
662
            case 'longDescription':
663
                return $model->getDescriptionLong() != $product->longDescription;
664
            case 'additionalDescription':
665
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
666
            case 'name':
667
                return $model->getName() != $product->title;
668
            case 'image':
669
                return count($model->getImages()) != count($product->images);
670
            case 'price':
671
                $prices = $detail->getPrices();
672
                if (empty($prices)) {
673
                    return true;
674
                }
675
                $price = $prices->first();
676
                if (!$price) {
677
                    return true;
678
                }
679
680
                return $prices->first()->getPrice() != $product->price;
681
        }
682
683
        throw new \InvalidArgumentException('Unrecognized field');
684
    }
685
686
    /**
687
     * @param array $updateFields
688
     * @param ProductModel $model
689
     * @param AttributeModel $detailAttribute
690
     * @param Product $product
691
     */
692
    private function setPropertiesForNewProducts(array $updateFields, ProductModel $model, AttributeModel $detailAttribute, Product $product)
693
    {
694
        /*
695
         * Make sure, that the following properties are set for
696
         * - new products
697
         * - products that have been configured to receive these updates
698
         */
699
        if ($updateFields['name']) {
700
            $model->setName($product->title);
701
        }
702
        if ($updateFields['shortDescription']) {
703
            $model->setDescription($product->shortDescription);
704
        }
705
        if ($updateFields['longDescription']) {
706
            $model->setDescriptionLong($product->longDescription);
707
        }
708
709
        if ($updateFields['additionalDescription']) {
710
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
711
        }
712
713
        if ($product->vat !== null) {
714
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
715
            $tax = round($product->vat * 100, 2);
716
            /** @var \Shopware\Models\Tax\Tax $tax */
717
            $tax = $repo->findOneBy(['tax' => $tax]);
718
            $model->setTax($tax);
719
        }
720
721
        if ($product->vendor !== null) {
722
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
723
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
724
            if ($supplier === null) {
725
                $supplier = $this->createSupplier($product->vendor);
726
            }
727
            $model->setSupplier($supplier);
728
        }
729
    }
730
731
    /**
732
     * @param $vendor
733
     * @return Supplier
734
     */
735
    private function createSupplier($vendor)
736
    {
737
        $supplier = new Supplier();
738
739
        if (is_array($vendor)) {
740
            $supplier->setName($vendor['name']);
741
            $supplier->setDescription($vendor['description']);
742
            if (array_key_exists('url', $vendor) && $vendor['url']) {
743
                $supplier->setLink($vendor['url']);
744
            }
745
746
            $supplier->setMetaTitle($vendor['page_title']);
747
748
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
749
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
750
            }
751
        } else {
752
            $supplier->setName($vendor);
753
        }
754
755
        //sets supplier attributes
756
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
757
        $attr->setConnectIsRemote(true);
758
759
        $supplier->setAttribute($attr);
760
761
        return $supplier;
762
    }
763
764
    /**
765
     * @param ProductModel $article
766
     * @param Product $product
767
     */
768
    private function applyProductProperties(ProductModel $article, Product $product)
769
    {
770
        if (empty($product->properties)) {
771
            return;
772
        }
773
774
        /** @var Property $firstProperty */
775
        $firstProperty = reset($product->properties);
776
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
777
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
778
779
        if (!$group) {
780
            $group = $this->createPropertyGroup($firstProperty);
781
        }
782
783
        $propertyValues = $article->getPropertyValues();
784
        $propertyValues->clear();
785
        $this->manager->persist($article);
786
        $this->manager->flush();
787
788
        $article->setPropertyGroup($group);
789
790
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
791
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
792
793
        foreach ($product->properties as $property) {
794
            $option = $optionRepo->findOneBy(['name' => $property->option]);
795
            $optionExists = $option instanceof PropertyOption;
0 ignored issues
show
Bug introduced by
The class Shopware\Models\Property\Option does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
796
            if (!$option) {
797
                $option = new PropertyOption();
798
                $option->setName($property->option);
799
                $option->setFilterable($property->filterable);
800
801
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
802
                $attribute->setPropertyOption($option);
803
                $attribute->setConnectIsRemote(true);
804
                $option->setAttribute($attribute);
805
806
                $this->manager->persist($option);
807
                $this->manager->flush($option);
808
            }
809
810
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
811
                $value = new PropertyValue($option, $property->value);
812
                $value->setPosition($property->valuePosition);
813
814
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
815
                $attribute->setPropertyValue($value);
816
                $attribute->setConnectIsRemote(true);
817
                $value->setAttribute($attribute);
818
819
                $this->manager->persist($value);
820
            }
821
822
            if (!$propertyValues->contains($value)) {
823
                //add only new values
824
                $propertyValues->add($value);
825
            }
826
827
            $filters = [
828
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
829
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
830
            ];
831
832
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
833
            $relation = $query->getOneOrNullResult();
834
835
            if (!$relation) {
836
                $group->addOption($option);
837
                $this->manager->persist($group);
838
                $this->manager->flush($group);
839
            }
840
        }
841
842
        $article->setPropertyValues($propertyValues);
843
844
        $this->manager->persist($article);
845
        $this->manager->flush();
846
    }
847
848
    /**
849
     * Read product attributes mapping and set to shopware attribute model
850
     *
851
     * @param AttributeModel $detailAttribute
852
     * @param Product $product
853
     * @return AttributeModel
854
     */
855
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
856
    {
857
        $detailAttribute->setConnectReference($product->sourceId);
858
        $detailAttribute->setConnectArticleShipping($product->shipping);
859
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
860
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
861
            $shopwareAttribute = $this->marketplaceGateway->findShopwareMappingFor($key);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $shopwareAttribute is correct as $this->marketplaceGatewa...hopwareMappingFor($key) (which targets ShopwarePlugins\Connect\...indShopwareMappingFor()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
862
            if (strlen($shopwareAttribute) > 0) {
863
                $setter = 'set' . ucfirst($shopwareAttribute);
864
                $detailAttribute->$setter($value);
865
            }
866
        });
867
868
        return $detailAttribute;
869
    }
870
871
    /**
872
     * @param  ConnectAttribute $connectAttribute
873
     * @param Product $product
874
     */
875
    private function setConnectAttributesFromProduct(ConnectAttribute $connectAttribute, Product $product)
876
    {
877
        $connectAttribute->setShopId($product->shopId);
878
        $connectAttribute->setSourceId($product->sourceId);
879
        $connectAttribute->setExportStatus(null);
880
        $connectAttribute->setPurchasePrice($product->purchasePrice);
881
        $connectAttribute->setFixedPrice($product->fixedPrice);
882
        $connectAttribute->setStream($product->stream);
883
884
        // store purchasePriceHash and offerValidUntil
885
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
886
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
887
    }
888
889
    /**
890
     * @param DetailModel $detail
891
     * @param Product $product
892
     */
893
    private function updateDetailFromProduct(DetailModel $detail, Product $product)
894
    {
895
        $detail->setInStock($product->availability);
896
        $detail->setEan($product->ean);
897
        $detail->setShippingTime($product->deliveryWorkDays);
898
        $releaseDate = new \DateTime();
899
        $releaseDate->setTimestamp($product->deliveryDate);
900
        $detail->setReleaseDate($releaseDate);
901
        $detail->setMinPurchase($product->minPurchaseQuantity);
902
    }
903
904
    /**
905
     * @param DetailModel $detail
906
     * @param Product $product
907
     * @param $detailAttribute
908
     */
909
    private function detailSetUnit(DetailModel $detail, Product $product, $detailAttribute)
910
    {
911
        // if connect product has unit
912
        // find local unit with units mapping
913
        // and add to detail model
914
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
915
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
916
            if ($this->config->getConfig($product->attributes['unit']) == null) {
917
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
918
            }
919
920
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
921
            $unitMapper = new UnitMapper($this->config, $this->manager);
922
923
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
924
925
            /** @var \Shopware\Models\Article\Unit $unit */
926
            $unit = $this->helper->getUnit($shopwareUnit);
927
            $detail->setUnit($unit);
928
            $detail->setPurchaseUnit($product->attributes['quantity']);
929
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
930
        } else {
931
            $detail->setUnit(null);
932
            $detail->setPurchaseUnit(null);
933
            $detail->setReferenceUnit(null);
934
        }
935
    }
936
937
    /**
938
     * @param DetailModel $detail
939
     * @param Product $product
940
     */
941
    private function detailSetAttributes(DetailModel $detail, Product $product)
942
    {
943
        // set dimension
944
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
945
            $dimension = explode('x', $product->attributes['dimension']);
946
            $detail->setLen($dimension[0]);
947
            $detail->setWidth($dimension[1]);
948
            $detail->setHeight($dimension[2]);
949
        } else {
950
            $detail->setLen(null);
951
            $detail->setWidth(null);
952
            $detail->setHeight(null);
953
        }
954
955
        // set weight
956
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
957
            $detail->setWeight($product->attributes['weight']);
958
        }
959
960
        //set package unit
961
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
962
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
963
        }
964
965
        //set basic unit
966
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
967
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
968
        }
969
970
        //set manufacturer no.
971
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
972
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
973
        }
974
    }
975
976
    /**
977
     * @param ConnectAttribute $connectAttribute
978
     * @param Product $product
979
     */
980
    private function connectAttributeSetLastUpdate(ConnectAttribute $connectAttribute, Product $product)
981
    {
982
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
983
        // This way a customer will be able to apply the most recent changes any time later
984
        $connectAttribute->setLastUpdate(json_encode([
985
            'shortDescription' => $product->shortDescription,
986
            'longDescription' => $product->longDescription,
987
            'additionalDescription' => $product->additionalDescription,
988
            'purchasePrice' => $product->purchasePrice,
989
            'image' => $product->images,
990
            'variantImages' => $product->variantImages,
991
            'price' => $product->price * ($product->vat + 1),
992
            'name' => $product->title,
993
            'vat' => $product->vat
994
        ]));
995
    }
996
997
    /**
998
     * @param ProductModel $model
999
     * @param array $categories
1000
     */
1001
    private function categoryDenormalization(ProductModel $model, array $categories)
1002
    {
1003
        $this->categoryDenormalization->disableTransactions();
1004
        foreach ($categories as $category) {
1005
            $this->categoryDenormalization->addAssignment($model->getId(), $category);
1006
            $this->manager->getConnection()->executeQuery(
1007
                'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?,?)',
1008
                [$model->getId(), $category]
1009
            );
1010
        }
1011
        $this->categoryDenormalization->enableTransactions();
1012
    }
1013
1014
    /**
1015
     * @param Product $product
1016
     * @return ProductStream
1017
     */
1018
    private function getOrCreateStream(Product $product)
1019
    {
1020
        /** @var ProductStreamRepository $repo */
1021
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
1022
        $stream = $repo->findConnectByName($product->stream);
1023
1024
        if (!$stream) {
1025
            $stream = new ProductStream();
1026
            $stream->setName($product->stream);
1027
            $stream->setType(ProductStreamService::STATIC_STREAM);
1028
            $stream->setSorting(json_encode(
1029
                [ReleaseDateSorting::class => ['direction' => 'desc']]
1030
            ));
1031
1032
            //add attributes
1033
            $attribute = new \Shopware\Models\Attribute\ProductStream();
1034
            $attribute->setProductStream($stream);
1035
            $attribute->setConnectIsRemote(true);
1036
            $stream->setAttribute($attribute);
1037
1038
            $this->manager->persist($attribute);
1039
            $this->manager->persist($stream);
1040
            $this->manager->flush();
1041
        }
1042
1043
        return $stream;
1044
    }
1045
1046
    /**
1047
     * @param ProductStream $stream
1048
     * @param ProductModel $article
1049
     * @throws \Doctrine\DBAL\DBALException
1050
     */
1051
    private function addProductToStream(ProductStream $stream, ProductModel $article)
1052
    {
1053
        $conn = $this->manager->getConnection();
1054
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
1055
                VALUES (:streamId, :articleId)
1056
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
1057
        $stmt = $conn->prepare($sql);
1058
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
1059
    }
1060
1061
    /**
1062
     * Set detail purchase price with plain SQL
1063
     * Entity usage throws exception when error handlers are disabled
1064
     *
1065
     * @param ProductModel $article
1066
     * @param DetailModel $detail
1067
     * @param Product $product
1068
     * @throws \Doctrine\DBAL\DBALException
1069
     */
1070
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
1071
    {
1072
        // set price via plain SQL because shopware throws exception
1073
        // undefined index: key when error handler is disabled
1074
        $customerGroup = $this->helper->getDefaultCustomerGroup();
1075
1076
        if (!empty($product->priceRanges)) {
1077
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
1078
1079
            return;
1080
        }
1081
1082
        $id = $this->manager->getConnection()->fetchColumn(
1083
            'SELECT id FROM `s_articles_prices`
1084
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1085
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
1086
        );
1087
1088
        // todo@sb: test update prices
1089
        if ($id > 0) {
1090
            $this->manager->getConnection()->executeQuery(
1091
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
1092
                [$product->price, $product->purchasePrice, $id]
1093
            );
1094
        } else {
1095
            $this->manager->getConnection()->executeQuery(
1096
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
1097
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
1098
                [
1099
                    $customerGroup->getKey(),
1100
                    $article->getId(),
1101
                    $detail->getId(),
1102
                    $product->price,
1103
                    $product->purchasePrice
1104
                ]
1105
            );
1106
        }
1107
    }
1108
1109
    /**
1110
     * @param ProductModel $article
1111
     * @param DetailModel $detail
1112
     * @param array $priceRanges
1113
     * @param Group $group
1114
     * @throws \Doctrine\DBAL\ConnectionException
1115
     * @throws \Exception
1116
     */
1117
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
1118
    {
1119
        $this->manager->getConnection()->beginTransaction();
1120
1121
        try {
1122
            // We always delete the prices,
1123
            // because we can not know which record is update
1124
            $this->manager->getConnection()->executeQuery(
1125
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
1126
                [$article->getId(), $detail->getId()]
1127
            );
1128
1129
            /** @var PriceRange $priceRange */
1130
            foreach ($priceRanges as $priceRange) {
1131
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
1132
1133
                //todo: maybe batch insert if possible?
1134
                $this->manager->getConnection()->executeQuery(
1135
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
1136
                      VALUES (?, ?, ?, ?, ?, ?);',
1137
                    [
1138
                        $group->getKey(),
1139
                        $priceRange->from,
1140
                        $priceTo,
1141
                        $article->getId(),
1142
                        $detail->getId(),
1143
                        $priceRange->price
1144
                    ]
1145
                );
1146
            }
1147
            $this->manager->getConnection()->commit();
1148
        } catch (\Exception $e) {
1149
            $this->manager->getConnection()->rollBack();
1150
            throw new \Exception($e->getMessage());
1151
        }
1152
    }
1153
1154
    /**
1155
     * Set detail purchase price with plain SQL
1156
     * Entity usage throws exception when error handlers are disabled
1157
     *
1158
     * @param DetailModel $detail
1159
     * @param float $purchasePrice
1160
     * @param Group $defaultGroup
1161
     * @throws \Doctrine\DBAL\DBALException
1162
     */
1163
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1164
    {
1165
        if (method_exists($detail, 'setPurchasePrice')) {
1166
            $this->manager->getConnection()->executeQuery(
1167
                'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1168
                [$purchasePrice, $detail->getId()]
1169
            );
1170
        } else {
1171
            $id = $this->manager->getConnection()->fetchColumn(
1172
                'SELECT id FROM `s_articles_prices`
1173
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1174
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1175
            );
1176
1177
            if ($id > 0) {
1178
                $this->manager->getConnection()->executeQuery(
1179
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1180
                    [$purchasePrice, $id]
1181
                );
1182
            } else {
1183
                $this->manager->getConnection()->executeQuery(
1184
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1185
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1186
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1187
                );
1188
            }
1189
        }
1190
    }
1191
1192
    /**
1193
     * Adds translation record for given article
1194
     *
1195
     * @param ProductModel $article
1196
     * @param Product $sdkProduct
1197
     */
1198
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
1199
    {
1200
        /** @var \Shopware\Connect\Struct\Translation $translation */
1201
        foreach ($sdkProduct->translations as $key => $translation) {
1202
            /** @var \Shopware\Models\Shop\Locale $locale */
1203
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
1204
            /** @var \Shopware\Models\Shop\Shop $shop */
1205
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
1206
            if (!$shop) {
1207
                continue;
1208
            }
1209
1210
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
1211
        }
1212
    }
1213
1214
    /**
1215
     * dsadsa
1216
     * @return \Shopware\Components\Model\ModelRepository
1217
     */
1218
    private function getLocaleRepository()
1219
    {
1220
        if (!$this->localeRepository) {
1221
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
1222
        }
1223
1224
        return $this->localeRepository;
1225
    }
1226
1227
    private function getShopRepository()
1228
    {
1229
        if (!$this->shopRepository) {
1230
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
1231
        }
1232
1233
        return $this->shopRepository;
1234
    }
1235
1236
    /**
1237
     * Delete product or product variant with given shopId and sourceId.
1238
     *
1239
     * Only the combination of both identifies a product uniquely. Do NOT
1240
     * delete products just by their sourceId.
1241
     *
1242
     * You might receive delete requests for products, which are not available
1243
     * in your shop. Just ignore them.
1244
     *
1245
     * @param string $shopId
1246
     * @param string $sourceId
1247
     * @return void
1248
     */
1249
    public function delete($shopId, $sourceId)
1250
    {
1251
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
1252
            'shopId' => $shopId,
1253
            'sourceId' => $sourceId,
1254
        ]));
1255
        if ($detail === null) {
1256
            return;
1257
        }
1258
1259
        $this->deleteDetail($detail);
1260
    }
1261
1262
    public function update($shopId, $sourceId, ProductUpdate $product)
1263
    {
1264
        // find article detail id
1265
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1266
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1267
            [$sourceId, $shopId]
1268
        );
1269
1270
        $this->eventManager->notify(
1271
            'Connect_Merchant_Update_GeneralProductInformation',
1272
            [
1273
                'subject' => $this,
1274
                'shopId' => $shopId,
1275
                'sourceId' => $sourceId,
1276
                'articleDetailId' => $articleDetailId
1277
            ]
1278
        );
1279
1280
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1281
        $this->manager->getConnection()->executeUpdate(
1282
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1283
            WHERE source_id = ? AND shop_id = ?',
1284
            [
1285
                $product->purchasePriceHash,
1286
                $product->offerValidUntil,
1287
                $product->purchasePrice,
1288
                $sourceId,
1289
                $shopId,
1290
            ]
1291
        );
1292
1293
        // update stock in article detail
1294
        // update prices
1295
        // if purchase price is stored in article detail
1296
        // update it together with stock
1297
        // since shopware 5.2
1298
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1299
            $this->manager->getConnection()->executeUpdate(
1300
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1301
                [$product->availability, $product->purchasePrice, $articleDetailId]
1302
            );
1303
        } else {
1304
            $this->manager->getConnection()->executeUpdate(
1305
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1306
                [$product->availability, $articleDetailId]
1307
            );
1308
        }
1309
        $this->manager->getConnection()->executeUpdate(
1310
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1311
            [$product->price, $product->purchasePrice, $articleDetailId]
1312
        );
1313
    }
1314
1315
    public function changeAvailability($shopId, $sourceId, $availability)
1316
    {
1317
        // find article detail id
1318
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1319
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1320
            [$sourceId, $shopId]
1321
        );
1322
1323
        $this->eventManager->notify(
1324
            'Connect_Merchant_Update_GeneralProductInformation',
1325
            [
1326
                'subject' => $this,
1327
                'shopId' => $shopId,
1328
                'sourceId' => $sourceId,
1329
                'articleDetailId' => $articleDetailId
1330
            ]
1331
        );
1332
1333
        // update stock in article detail
1334
        $this->manager->getConnection()->executeUpdate(
1335
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1336
            [$availability, $articleDetailId]
1337
        );
1338
    }
1339
1340
    /**
1341
     * @inheritDoc
1342
     */
1343
    public function makeMainVariant($shopId, $sourceId, $groupId)
1344
    {
1345
        //find article detail which should be selected as main one
1346
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1347
        if (!$newMainDetail) {
1348
            return;
1349
        }
1350
1351
        /** @var \Shopware\Models\Article\Article $article */
1352
        $article = $newMainDetail->getArticle();
1353
1354
        $this->eventManager->notify(
1355
            'Connect_Merchant_Update_ProductMainVariant_Before',
1356
            [
1357
                'subject' => $this,
1358
                'shopId' => $shopId,
1359
                'sourceId' => $sourceId,
1360
                'articleId' => $article->getId(),
1361
                'articleDetailId' => $newMainDetail->getId()
1362
            ]
1363
        );
1364
1365
        // replace current main detail with new one
1366
        $currentMainDetail = $article->getMainDetail();
1367
        $currentMainDetail->setKind(2);
1368
        $newMainDetail->setKind(1);
1369
        $article->setMainDetail($newMainDetail);
1370
1371
        $this->manager->persist($newMainDetail);
1372
        $this->manager->persist($currentMainDetail);
1373
        $this->manager->persist($article);
1374
        $this->manager->flush();
1375
    }
1376
1377
    /**
1378
     * Updates the status of an Order
1379
     *
1380
     * @param string $localOrderId
1381
     * @param string $orderStatus
1382
     * @param string $trackingNumber
1383
     * @return void
1384
     */
1385
    public function updateOrderStatus($localOrderId, $orderStatus, $trackingNumber)
1386
    {
1387
        if ($this->config->getConfig('updateOrderStatus') == 1) {
1388
            $this->updateDeliveryStatus($localOrderId, $orderStatus);
1389
        }
1390
1391
        if ($trackingNumber) {
1392
            $this->updateTrackingNumber($localOrderId, $trackingNumber);
1393
        }
1394
    }
1395
1396
    /**
1397
     * @param string $localOrderId
1398
     * @param string $orderStatus
1399
     */
1400
    private function updateDeliveryStatus($localOrderId, $orderStatus)
1401
    {
1402
        $status = false;
1403
        if ($orderStatus === OrderStatus::STATE_IN_PROCESS) {
1404
            $status = Status::ORDER_STATE_PARTIALLY_DELIVERED;
1405
        } elseif ($orderStatus === OrderStatus::STATE_DELIVERED) {
1406
            $status = Status::ORDER_STATE_COMPLETELY_DELIVERED;
1407
        }
1408
1409
        if ($status) {
1410
            $this->manager->getConnection()->executeQuery(
1411
                'UPDATE s_order 
1412
                SET status = :orderStatus
1413
                WHERE ordernumber = :orderNumber',
1414
                [
1415
                    ':orderStatus' => $status,
1416
                    ':orderNumber' => $localOrderId
1417
                ]
1418
            );
1419
        }
1420
    }
1421
1422
    /**
1423
     * @param string $localOrderId
1424
     * @param string $trackingNumber
1425
     */
1426
    private function updateTrackingNumber($localOrderId, $trackingNumber)
1427
    {
1428
        $currentTrackingCode = $this->manager->getConnection()->fetchColumn(
1429
            'SELECT trackingcode
1430
            FROM s_order
1431
            WHERE ordernumber = :orderNumber',
1432
            [
1433
                ':orderNumber' => $localOrderId
1434
            ]
1435
        );
1436
1437
        if (!$currentTrackingCode) {
1438
            $newTracking = $trackingNumber;
1439
        } else {
1440
            $newTracking = $this->combineTrackingNumbers($trackingNumber, $currentTrackingCode);
1441
        }
1442
1443
        $this->manager->getConnection()->executeQuery(
1444
            'UPDATE s_order 
1445
            SET trackingcode = :trackingCode
1446
            WHERE ordernumber = :orderNumber',
1447
            [
1448
                ':trackingCode' => $newTracking,
1449
                ':orderNumber' => $localOrderId
1450
            ]
1451
        );
1452
    }
1453
1454
    /**
1455
     * @param string $newTrackingCode
1456
     * @param string $currentTrackingCode
1457
     * @return string
1458
     */
1459
    private function combineTrackingNumbers($newTrackingCode, $currentTrackingCode)
1460
    {
1461
        $currentTrackingCodes = $this->getTrackingNumberAsArray($currentTrackingCode);
1462
        $newTrackingCodes = $this->getTrackingNumberAsArray($newTrackingCode);
1463
        $newTrackingCodes = array_unique(array_merge($currentTrackingCodes, $newTrackingCodes));
1464
        $newTracking = implode(',', $newTrackingCodes);
1465
1466
        return $newTracking;
1467
    }
1468
1469
    /**
1470
     * @param string $trackingCode
1471
     * @return string[]
1472
     */
1473
    private function getTrackingNumberAsArray($trackingCode)
1474
    {
1475
        if (strpos($trackingCode, ',') !== false) {
1476
            return explode(',', $trackingCode);
1477
        }
1478
1479
        return [$trackingCode];
1480
    }
1481
1482
    /**
1483
     * @param Product $product
1484
     * @param ProductModel $model
1485
     */
1486
    private function saveVat(Product $product, ProductModel $model)
1487
    {
1488
        if ($product->vat !== null) {
1489
            $repo = $this->manager->getRepository(Tax::class);
1490
            $taxRate = round($product->vat * 100, 2);
1491
            /** @var \Shopware\Models\Tax\Tax $tax */
1492
            $tax = $repo->findOneBy(['tax' => $taxRate]);
1493
            if (!$tax) {
1494
                $tax = new Tax();
1495
                $tax->setTax($taxRate);
1496
                //this is to get rid of zeroes behind the decimal point
1497
                $name = strval(round($taxRate, 2)) . '%';
1498
                $tax->setName($name);
1499
                $this->manager->persist($tax);
1500
            }
1501
            $model->setTax($tax);
1502
        }
1503
    }
1504
1505
    /**
1506
     * @param Property $property
1507
     * @return PropertyGroup
1508
     */
1509
    private function createPropertyGroup(Property $property)
1510
    {
1511
        $group = new PropertyGroup();
1512
        $group->setName($property->groupName);
1513
        $group->setComparable($property->comparable);
1514
        $group->setSortMode($property->sortMode);
1515
        $group->setPosition($property->groupPosition);
1516
1517
        $attribute = new \Shopware\Models\Attribute\PropertyGroup();
1518
        $attribute->setPropertyGroup($group);
1519
        $attribute->setConnectIsRemote(true);
1520
        $group->setAttribute($attribute);
1521
1522
        $this->manager->persist($attribute);
1523
        $this->manager->persist($group);
1524
        $this->manager->flush();
1525
1526
        return $group;
1527
    }
1528
}
1529