Completed
Push — master ( 335c44...a95920 )
by Jonas
05:50 queued 02:51
created

ProductToShop::updateDetailFromProduct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 2
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
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
        $detailAttribute = $this->getOrCreateAttributeModel($detail, $model);
233
234
        $connectAttribute = $this->helper->getConnectAttributeByModel($detail) ?: new ConnectAttribute;
235
        // configure main variant and groupId
236
        if ($isMainVariant === true) {
237
            $connectAttribute->setIsMainVariant(true);
238
        }
239
        $connectAttribute->setGroupId($product->groupId);
240
241
        list($updateFields, $flag) = $this->getUpdateFields($model, $detail, $connectAttribute, $product);
242
        $this->setPropertiesForNewProducts($updateFields, $model, $detailAttribute, $product);
243
244
        $this->saveVat($product, $model);
245
246
        $this->applyProductProperties($model, $product);
247
248
        $detailAttribute = $this->applyMarketplaceAttributes($detailAttribute, $product);
249
250
        $this->setConnectAttributesFromProduct($connectAttribute, $product);
251
252
        // store product categories to connect attribute
253
        $connectAttribute->setCategory($product->categories);
254
255
        $connectAttribute->setLastUpdateFlag($flag);
256
257
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
258
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
259
260
        $this->updateDetailFromProduct($detail, $product);
261
262
        // some shops have feature "sell not in stock",
263
        // then end customer should be able to by the product with stock = 0
264
        $shopConfiguration = $this->connectGateway->getShopConfiguration($product->shopId);
265
        if ($shopConfiguration && $shopConfiguration->sellNotInStock) {
266
            $model->setLastStock(false);
267
        } else {
268
            $model->setLastStock(true);
269
        }
270
271
        $this->detailSetUnit($detail, $product, $detailAttribute);
272
273
        $this->detailSetAttributes($detail, $product);
274
275
        $this->connectAttributeSetLastUpdate($connectAttribute, $product);
276
277
        if ($model->getMainDetail() === null) {
278
            $model->setMainDetail($detail);
279
        }
280
281
        if ($detail->getAttribute() === null) {
282
            $detail->setAttribute($detailAttribute);
283
            $detailAttribute->setArticle($model);
284
        }
285
286
        $connectAttribute->setArticle($model);
287
        $connectAttribute->setArticleDetail($detail);
288
289
        $this->eventManager->notify(
290
            'Connect_Merchant_Saving_ArticleAttribute_Before',
291
            [
292
                'subject' => $this,
293
                'connectAttribute' => $connectAttribute
294
            ]
295
        );
296
297
        //article has to be flushed
298
        $this->manager->persist($model);
299
        $this->manager->persist($connectAttribute);
300
        $this->manager->persist($detail);
301
        $this->manager->flush();
302
303
        $this->categoryResolver->storeRemoteCategories($product->categories, $model->getId(), $product->shopId);
304
        $categories = $this->categoryResolver->resolve($product->categories, $product->shopId);
305
        if (count($categories) > 0) {
306
            $detailAttribute->setConnectMappedCategory(true);
307
        }
308
309
        $this->manager->persist($detailAttribute);
310
        $this->manager->flush();
311
312
        $this->categoryDenormalization($model, $categories);
313
314
        $defaultCustomerGroup = $this->helper->getDefaultCustomerGroup();
315
        // Only set prices, if fixedPrice is active or price updates are configured
316
        if (count($detail->getPrices()) == 0 || $connectAttribute->getFixedPrice() || $updateFields['price']) {
317
            $this->setPrice($model, $detail, $product);
318
        }
319
        // If the price is not being update, update the purchasePrice anyway
320
        $this->setPurchasePrice($detail, $product->purchasePrice, $defaultCustomerGroup);
321
322
        $this->manager->clear();
323
324
        $this->addArticleTranslations($model, $product);
325
326
        //clear cache for that article
327
        $this->helper->clearArticleCache($model->getId());
328
329
        if ($updateFields['image']) {
330
            // Reload the model in order to not to work an the already flushed model
331
            $model = $this->helper->getArticleModelByProduct($product);
332
            // import only global images for article
333
            $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 331 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...
334
            if ($updateFields['mainImage'] && isset($product->images[0])) {
335
                $this->imageImport->importMainImage($product->images[0], $model->getId());
336
            }
337
            // Reload the article detail model in order to not to work an the already flushed model
338
            $detail = $this->helper->getArticleDetailModelByProduct($product);
339
            // import only specific images for variant
340
            $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 338 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...
341
        }
342
343
        $this->eventManager->notify(
344
            'Connect_ProductToShop_InsertOrUpdate_After',
345
            [
346
                'connectProduct' => $product,
347
                'shopArticleDetail' => $detail
348
            ]
349
        );
350
351
        $stream = $this->getOrCreateStream($product);
352
        $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...
353
    }
354
355
    /**
356
     * @param Product $product
357
     * @return string
358
     */
359
    private function generateSKU(Product $product)
360
    {
361
        if (!empty($product->sku)) {
362
            $number = 'SC-' . $product->shopId . '-' . $product->sku;
363
            $duplicatedDetail = $this->helper->getDetailByNumber($number);
364
            if ($duplicatedDetail
365
                && $this->helper->getConnectAttributeByModel($duplicatedDetail)->getSourceId() != $product->sourceId
366
            ) {
367
                $this->deleteDetail($duplicatedDetail);
368
            }
369
        } else {
370
            $number = 'SC-' . $product->shopId . '-' . $product->sourceId;
371
        }
372
373
        return $number;
374
    }
375
376
    /**
377
     * @param DetailModel $detailModel
378
     */
379
    private function deleteDetail(DetailModel $detailModel)
380
    {
381
        $this->eventManager->notify(
382
            'Connect_Merchant_Delete_Product_Before',
383
            [
384
                'subject' => $this,
385
                'articleDetail' => $detailModel
386
            ]
387
        );
388
389
        $article = $detailModel->getArticle();
390
        // Not sure why, but the Attribute can be NULL
391
        $attribute = $this->helper->getConnectAttributeByModel($detailModel);
392
        $this->manager->remove($detailModel);
393
394
        if ($attribute) {
395
            $this->manager->remove($attribute);
396
        }
397
398
        // if removed variant is main variant
399
        // find first variant which is not main and mark it
400
        if ($detailModel->getKind() === 1) {
401
            /** @var \Shopware\Models\Article\Detail $variant */
402
            foreach ($article->getDetails() as $variant) {
403
                if ($variant->getId() != $detailModel->getId()) {
404
                    $variant->setKind(1);
405
                    $article->setMainDetail($variant);
406
                    $connectAttribute = $this->helper->getConnectAttributeByModel($variant);
407
                    if (!$connectAttribute) {
408
                        continue;
409
                    }
410
                    $connectAttribute->setIsMainVariant(true);
411
                    $this->manager->persist($connectAttribute);
412
                    $this->manager->persist($article);
413
                    $this->manager->persist($variant);
414
                    break;
415
                }
416
            }
417
        }
418
419
        if (count($details = $article->getDetails()) === 1) {
420
            $details->clear();
421
            $this->manager->remove($article);
422
        }
423
424
        //save category Ids before flush
425
        $oldCategoryIds = array_map(function ($category) {
426
            return $category->getId();
427
        }, $article->getCategories()->toArray());
428
429
        // Do not remove flush. It's needed when remove article,
430
        // because duplication of ordernumber. Even with remove before
431
        // persist calls mysql throws exception "Duplicate entry"
432
        $this->manager->flush();
433
        // always clear entity manager, because $article->getDetails() returns
434
        // more than 1 detail, but all of them were removed except main one.
435
        $this->manager->clear();
436
437
        // call this after flush because article has to be deleted that this works
438
        if (count($oldCategoryIds) > 0) {
439
            $this->categoryResolver->deleteEmptyConnectCategories($oldCategoryIds);
440
        }
441
    }
442
443
    /**
444
     * @param Product $product
445
     * @param $active
446
     * @param $isMainVariant
447
     * @return null|Article
448
     */
449
    private function getSWProductModel(Product $product, $active, &$isMainVariant)
450
    {
451
        if ($product->groupId !== null) {
452
            $model = $this->helper->getArticleByRemoteProduct($product);
453
            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...
454
                $model = $this->helper->createProductModel($product);
455
                $model->setActive($active);
456
                $isMainVariant = true;
457
            }
458
        } else {
459
            $model = $this->helper->getConnectArticleModel($product->sourceId, $product->shopId);
460
            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...
461
                $model = $this->helper->createProductModel($product);
462
                $model->setActive($active);
463
            }
464
        }
465
466
        return $model;
467
    }
468
469
    /**
470
     * @param Product $product
471
     * @param $model
472
     * @return DetailModel
473
     */
474
    private function generateNewDetail(Product $product, $model)
475
    {
476
        $detail = new DetailModel();
477
        $detail->setActive($model->getActive());
478
        $this->manager->persist($detail);
479
        $detail->setArticle($model);
480
        $model->getDetails()->add($detail);
481
        if (!empty($product->variant)) {
482
            $this->variantConfigurator->configureVariantAttributes($product, $detail);
483
        }
484
485
        return $detail;
486
    }
487
488
    /**
489
     * @param DetailModel $detail
490
     * @param DetailModel $mainDetail
491
     * @return bool
492
     */
493
    private function checkIfMainVariant(DetailModel $detail, DetailModel $mainDetail)
494
    {
495
        return $detail->getId() === $mainDetail->getId();
496
    }
497
498
    /**
499
     * @param ProductModel $model
500
     * @param Product $product
501
     */
502
    private function updateConfiguratorSetTypeFromProduct(ProductModel $model, Product $product)
503
    {
504
        if (!empty($product->variant)) {
505
            $configSet = $model->getConfiguratorSet();
506
            $configSet->setType($product->configuratorSetType);
507
        }
508
    }
509
510
    /**
511
     * @param ProductModel $model
512
     * @param Product $product
513
     */
514
    private function cleanUpConfiguratorSet(ProductModel $model, Product $product)
515
    {
516
        if (empty($product->variant) && $model->getConfiguratorSet()) {
517
            $this->manager->getConnection()->executeQuery(
518
                'UPDATE s_articles SET configurator_set_id = NULL WHERE id = ?',
519
                [$model->getId()]
520
            );
521
        }
522
    }
523
524
    /**
525
     * @param DetailModel $detail
526
     * @param ProductModel $model
527
     * @return AttributeModel
528
     */
529
    private function getOrCreateAttributeModel(DetailModel $detail, ProductModel $model)
530
    {
531
        $detailAttribute = $detail->getAttribute();
532
        if (!$detailAttribute) {
533
            $detailAttribute = new AttributeModel();
534
            $detail->setAttribute($detailAttribute);
535
            $model->setAttribute($detailAttribute);
536
            $detailAttribute->setArticle($model);
537
            $detailAttribute->setArticleDetail($detail);
538
        }
539
540
        return $detailAttribute;
541
    }
542
543
    /**
544
     * Get array of update info for the known fields
545
     *
546
     * @param $model
547
     * @param $detail
548
     * @param $attribute
549
     * @param $product
550
     * @return array
551
     */
552
    public function getUpdateFields($model, $detail, $attribute, $product)
553
    {
554
        // This also defines the flags of these fields
555
        $fields = $this->helper->getUpdateFlags();
556
        $flagsByName = array_flip($fields);
557
558
        $flag = 0;
559
        $output = [];
560
        foreach ($fields as $key => $field) {
561
            // Don't handle the imageInitialImport flag
562
            if ($field == 'imageInitialImport') {
563
                continue;
564
            }
565
566
            // If this is a new product
567
            if (!$model->getId() && $field == 'image' && !$this->config->getConfig('importImagesOnFirstImport',
568
                    false)) {
569
                $output[$field] = false;
570
                $flag |= $flagsByName['imageInitialImport'];
571
                continue;
572
            }
573
574
            $updateAllowed = $this->isFieldUpdateAllowed($field, $model, $attribute);
575
            $output[$field] = $updateAllowed;
576
            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...
577
                $flag |= $key;
578
            }
579
        }
580
581
        return [$output, $flag];
582
    }
583
584
    /**
585
     * Helper method to determine if a given $fields may/must be updated.
586
     * This method will check for the model->id in order to determine, if it is a new entity. Therefore
587
     * this method cannot be used after the model in question was already flushed.
588
     *
589
     * @param $field
590
     * @param $model ProductModel
591
     * @param $attribute ConnectAttribute
592
     * @throws \RuntimeException
593
     * @return bool|null
594
     */
595
    public function isFieldUpdateAllowed($field, ProductModel $model, ConnectAttribute $attribute)
596
    {
597
        $allowed = [
598
            'ShortDescription',
599
            'LongDescription',
600
            'AdditionalDescription',
601
            'Image',
602
            'Price',
603
            'Name',
604
            'MainImage',
605
        ];
606
607
        // Always allow updates for new models
608
        if (!$model->getId()) {
609
            return true;
610
        }
611
612
        $field = ucfirst($field);
613
        $attributeGetter = 'getUpdate' . $field;
614
        $configName = 'overwriteProduct' . $field;
615
616
        if (!in_array($field, $allowed)) {
617
            throw new \RuntimeException("Unknown update field {$field}");
618
        }
619
620
        $attributeValue = $attribute->$attributeGetter();
621
622
623
        // If the value is 'null' or 'inherit', the behaviour will be inherited from the global configuration
624
        // Once we have a supplier based configuration, we need to take it into account here
625
        if ($attributeValue == null || $attributeValue == 'inherit') {
626
            return $this->config->getConfig($configName, true);
627
        }
628
629
        return $attributeValue == 'overwrite';
630
    }
631
632
    /**
633
     * Determine if a given field has changed
634
     *
635
     * @param $field
636
     * @param ProductModel $model
637
     * @param DetailModel $detail
638
     * @param Product $product
639
     * @return bool
640
     */
641
    public function hasFieldChanged($field, ProductModel $model, DetailModel $detail, Product $product)
642
    {
643
        switch ($field) {
644
            case 'shortDescription':
645
                return $model->getDescription() != $product->shortDescription;
646
            case 'longDescription':
647
                return $model->getDescriptionLong() != $product->longDescription;
648
            case 'additionalDescription':
649
                return $detail->getAttribute()->getConnectProductDescription() != $product->additionalDescription;
650
            case 'name':
651
                return $model->getName() != $product->title;
652
            case 'image':
653
                return count($model->getImages()) != count($product->images);
654
            case 'mainImage':
655
                if ($product->images[0]) {
656
                    return $this->imageImport->hasMainImageChanged($product->images[0], $model->getId());
657
                }
658
659
                return false;
660
            case 'price':
661
                $prices = $detail->getPrices();
662
                if (empty($prices)) {
663
                    return true;
664
                }
665
                $price = $prices->first();
666
                if (!$price) {
667
                    return true;
668
                }
669
670
                return $prices->first()->getPrice() != $product->price;
671
        }
672
673
        throw new \InvalidArgumentException('Unrecognized field');
674
    }
675
676
    /**
677
     * @param array $updateFields
678
     * @param ProductModel $model
679
     * @param AttributeModel $detailAttribute
680
     * @param Product $product
681
     */
682
    private function setPropertiesForNewProducts(array $updateFields, ProductModel $model, AttributeModel $detailAttribute, Product $product)
683
    {
684
        /*
685
         * Make sure, that the following properties are set for
686
         * - new products
687
         * - products that have been configured to receive these updates
688
         */
689
        if ($updateFields['name']) {
690
            $model->setName($product->title);
691
        }
692
        if ($updateFields['shortDescription']) {
693
            $model->setDescription($product->shortDescription);
694
        }
695
        if ($updateFields['longDescription']) {
696
            $model->setDescriptionLong($product->longDescription);
697
        }
698
699
        if ($updateFields['additionalDescription']) {
700
            $detailAttribute->setConnectProductDescription($product->additionalDescription);
701
        }
702
703
        if ($product->vat !== null) {
704
            $repo = $this->manager->getRepository('Shopware\Models\Tax\Tax');
705
            $tax = round($product->vat * 100, 2);
706
            /** @var \Shopware\Models\Tax\Tax $tax */
707
            $tax = $repo->findOneBy(['tax' => $tax]);
708
            $model->setTax($tax);
709
        }
710
711
        if ($product->vendor !== null) {
712
            $repo = $this->manager->getRepository('Shopware\Models\Article\Supplier');
713
            $supplier = $repo->findOneBy(['name' => $product->vendor]);
714
            if ($supplier === null) {
715
                $supplier = $this->createSupplier($product->vendor);
716
            }
717
            $model->setSupplier($supplier);
718
        }
719
    }
720
721
    /**
722
     * @param $vendor
723
     * @return Supplier
724
     */
725
    private function createSupplier($vendor)
726
    {
727
        $supplier = new Supplier();
728
729
        if (is_array($vendor)) {
730
            $supplier->setName($vendor['name']);
731
            $supplier->setDescription($vendor['description']);
732
            if (array_key_exists('url', $vendor) && $vendor['url']) {
733
                $supplier->setLink($vendor['url']);
734
            }
735
736
            $supplier->setMetaTitle($vendor['page_title']);
737
738
            if (array_key_exists('logo_url', $vendor) && $vendor['logo_url']) {
739
                $this->imageImport->importImageForSupplier($vendor['logo_url'], $supplier);
740
            }
741
        } else {
742
            $supplier->setName($vendor);
743
        }
744
745
        //sets supplier attributes
746
        $attr = new \Shopware\Models\Attribute\ArticleSupplier();
747
        $attr->setConnectIsRemote(true);
748
749
        $supplier->setAttribute($attr);
750
751
        return $supplier;
752
    }
753
754
    /**
755
     * @param ProductModel $article
756
     * @param Product $product
757
     */
758
    private function applyProductProperties(ProductModel $article, Product $product)
759
    {
760
        if (empty($product->properties)) {
761
            return;
762
        }
763
764
        /** @var Property $firstProperty */
765
        $firstProperty = reset($product->properties);
766
        $groupRepo = $this->manager->getRepository(PropertyGroup::class);
767
        $group = $groupRepo->findOneBy(['name' => $firstProperty->groupName]);
768
769
        if (!$group) {
770
            $group = $this->createPropertyGroup($firstProperty);
771
        }
772
773
        $propertyValues = $article->getPropertyValues();
774
        $propertyValues->clear();
775
        $this->manager->persist($article);
776
        $this->manager->flush();
777
778
        $article->setPropertyGroup($group);
779
780
        $optionRepo = $this->manager->getRepository(PropertyOption::class);
781
        $valueRepo = $this->manager->getRepository(PropertyValue::class);
782
783
        foreach ($product->properties as $property) {
784
            $option = $optionRepo->findOneBy(['name' => $property->option]);
785
            $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...
786
            if (!$option) {
787
                $option = new PropertyOption();
788
                $option->setName($property->option);
789
                $option->setFilterable($property->filterable);
790
791
                $attribute = new \Shopware\Models\Attribute\PropertyOption();
792
                $attribute->setPropertyOption($option);
793
                $attribute->setConnectIsRemote(true);
794
                $option->setAttribute($attribute);
795
796
                $this->manager->persist($option);
797
                $this->manager->flush($option);
798
            }
799
800
            if (!$optionExists || !$value = $valueRepo->findOneBy(['option' => $option, 'value' => $property->value])) {
801
                $value = new PropertyValue($option, $property->value);
802
                $value->setPosition($property->valuePosition);
803
804
                $attribute = new \Shopware\Models\Attribute\PropertyValue();
805
                $attribute->setPropertyValue($value);
806
                $attribute->setConnectIsRemote(true);
807
                $value->setAttribute($attribute);
808
809
                $this->manager->persist($value);
810
            }
811
812
            if (!$propertyValues->contains($value)) {
813
                //add only new values
814
                $propertyValues->add($value);
815
            }
816
817
            $filters = [
818
                ['property' => 'options.name', 'expression' => '=', 'value' => $property->option],
819
                ['property' => 'groups.name', 'expression' => '=', 'value' => $property->groupName],
820
            ];
821
822
            $query = $groupRepo->getPropertyRelationQuery($filters, null, 1, 0);
823
            $relation = $query->getOneOrNullResult();
824
825
            if (!$relation) {
826
                $group->addOption($option);
827
                $this->manager->persist($group);
828
                $this->manager->flush($group);
829
            }
830
        }
831
832
        $article->setPropertyValues($propertyValues);
833
834
        $this->manager->persist($article);
835
        $this->manager->flush();
836
    }
837
838
    /**
839
     * Read product attributes mapping and set to shopware attribute model
840
     *
841
     * @param AttributeModel $detailAttribute
842
     * @param Product $product
843
     * @return AttributeModel
844
     */
845
    private function applyMarketplaceAttributes(AttributeModel $detailAttribute, Product $product)
846
    {
847
        $detailAttribute->setConnectReference($product->sourceId);
848
        $detailAttribute->setConnectArticleShipping($product->shipping);
849
        //todo@sb: check if connectAttribute matches position of the marketplace attribute
850
        array_walk($product->attributes, function ($value, $key) use ($detailAttribute) {
851
            $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...
852
            if (strlen($shopwareAttribute) > 0) {
853
                $setter = 'set' . ucfirst($shopwareAttribute);
854
                $detailAttribute->$setter($value);
855
            }
856
        });
857
858
        return $detailAttribute;
859
    }
860
861
    /**
862
     * @param  ConnectAttribute $connectAttribute
863
     * @param Product $product
864
     */
865
    private function setConnectAttributesFromProduct(ConnectAttribute $connectAttribute, Product $product)
866
    {
867
        $connectAttribute->setShopId($product->shopId);
868
        $connectAttribute->setSourceId($product->sourceId);
869
        $connectAttribute->setExportStatus(null);
870
        $connectAttribute->setPurchasePrice($product->purchasePrice);
871
        $connectAttribute->setFixedPrice($product->fixedPrice);
872
        $connectAttribute->setStream($product->stream);
873
874
        // store purchasePriceHash and offerValidUntil
875
        $connectAttribute->setPurchasePriceHash($product->purchasePriceHash);
876
        $connectAttribute->setOfferValidUntil($product->offerValidUntil);
877
    }
878
879
    /**
880
     * @param DetailModel $detail
881
     * @param Product $product
882
     */
883
    private function updateDetailFromProduct(DetailModel $detail, Product $product)
884
    {
885
        $detail->setInStock($product->availability);
886
        $detail->setEan($product->ean);
887
        $detail->setShippingTime($product->deliveryWorkDays);
888
        $releaseDate = new \DateTime();
889
        $releaseDate->setTimestamp($product->deliveryDate);
890
        $detail->setReleaseDate($releaseDate);
891
        $detail->setMinPurchase($product->minPurchaseQuantity);
892
    }
893
894
    /**
895
     * @param DetailModel $detail
896
     * @param Product $product
897
     * @param $detailAttribute
898
     */
899
    private function detailSetUnit(DetailModel $detail, Product $product, $detailAttribute)
900
    {
901
        // if connect product has unit
902
        // find local unit with units mapping
903
        // and add to detail model
904
        if (array_key_exists('unit', $product->attributes) && $product->attributes['unit']) {
905
            $detailAttribute->setConnectRemoteUnit($product->attributes['unit']);
906
            if ($this->config->getConfig($product->attributes['unit']) == null) {
907
                $this->config->setConfig($product->attributes['unit'], '', null, 'units');
908
            }
909
910
            /** @var \ShopwarePlugins\Connect\Components\Utils\UnitMapper $unitMapper */
911
            $unitMapper = new UnitMapper($this->config, $this->manager);
912
913
            $shopwareUnit = $unitMapper->getShopwareUnit($product->attributes['unit']);
914
915
            /** @var \Shopware\Models\Article\Unit $unit */
916
            $unit = $this->helper->getUnit($shopwareUnit);
917
            $detail->setUnit($unit);
918
            $detail->setPurchaseUnit($product->attributes['quantity']);
919
            $detail->setReferenceUnit($product->attributes['ref_quantity']);
920
        } else {
921
            $detail->setUnit(null);
922
            $detail->setPurchaseUnit(null);
923
            $detail->setReferenceUnit(null);
924
        }
925
    }
926
927
    /**
928
     * @param DetailModel $detail
929
     * @param Product $product
930
     */
931
    private function detailSetAttributes(DetailModel $detail, Product $product)
932
    {
933
        // set dimension
934
        if (array_key_exists('dimension', $product->attributes) && $product->attributes['dimension']) {
935
            $dimension = explode('x', $product->attributes['dimension']);
936
            $detail->setLen($dimension[0]);
937
            $detail->setWidth($dimension[1]);
938
            $detail->setHeight($dimension[2]);
939
        } else {
940
            $detail->setLen(null);
941
            $detail->setWidth(null);
942
            $detail->setHeight(null);
943
        }
944
945
        // set weight
946
        if (array_key_exists('weight', $product->attributes) && $product->attributes['weight']) {
947
            $detail->setWeight($product->attributes['weight']);
948
        }
949
950
        //set package unit
951
        if (array_key_exists(Product::ATTRIBUTE_PACKAGEUNIT, $product->attributes)) {
952
            $detail->setPackUnit($product->attributes[Product::ATTRIBUTE_PACKAGEUNIT]);
953
        }
954
955
        //set basic unit
956
        if (array_key_exists(Product::ATTRIBUTE_BASICUNIT, $product->attributes)) {
957
            $detail->setMinPurchase($product->attributes[Product::ATTRIBUTE_BASICUNIT]);
958
        }
959
960
        //set manufacturer no.
961
        if (array_key_exists(Product::ATTRIBUTE_MANUFACTURERNUMBER, $product->attributes)) {
962
            $detail->setSupplierNumber($product->attributes[Product::ATTRIBUTE_MANUFACTURERNUMBER]);
963
        }
964
    }
965
966
    /**
967
     * @param ConnectAttribute $connectAttribute
968
     * @param Product $product
969
     */
970
    private function connectAttributeSetLastUpdate(ConnectAttribute $connectAttribute, Product $product)
971
    {
972
        // Whenever a product is updated, store a json encoded list of all fields that are updated optionally
973
        // This way a customer will be able to apply the most recent changes any time later
974
        $connectAttribute->setLastUpdate(json_encode([
975
            'shortDescription' => $product->shortDescription,
976
            'longDescription' => $product->longDescription,
977
            'additionalDescription' => $product->additionalDescription,
978
            'purchasePrice' => $product->purchasePrice,
979
            'image' => $product->images,
980
            'variantImages' => $product->variantImages,
981
            'price' => $product->price * ($product->vat + 1),
982
            'name' => $product->title,
983
            'vat' => $product->vat
984
        ]));
985
    }
986
987
    /**
988
     * @param ProductModel $model
989
     * @param array $categories
990
     */
991
    private function categoryDenormalization(ProductModel $model, array $categories)
992
    {
993
        $this->categoryDenormalization->disableTransactions();
994
        foreach ($categories as $category) {
995
            $this->categoryDenormalization->addAssignment($model->getId(), $category);
996
            $this->manager->getConnection()->executeQuery(
997
                'INSERT IGNORE INTO `s_articles_categories` (`articleID`, `categoryID`) VALUES (?,?)',
998
                [$model->getId(), $category]
999
            );
1000
            $parentId =$this->manager->getConnection()->fetchColumn(
1001
                'SELECT parent FROM `s_categories` WHERE id = ?',
1002
                [$category]
1003
            );
1004
            $this->categoryDenormalization->removeAssignment($model->getId(), $parentId);
1005
            $this->manager->getConnection()->executeQuery(
1006
                'DELETE FROM `s_articles_categories` WHERE `articleID` = ? AND `categoryID` = ?',
1007
                [$model->getId(), $parentId]
1008
            );
1009
        }
1010
        $this->categoryDenormalization->enableTransactions();
1011
    }
1012
1013
    /**
1014
     * @param Product $product
1015
     * @return ProductStream
1016
     */
1017
    private function getOrCreateStream(Product $product)
1018
    {
1019
        /** @var ProductStreamRepository $repo */
1020
        $repo = $this->manager->getRepository(ProductStreamAttribute::class);
1021
        $stream = $repo->findConnectByName($product->stream);
1022
1023
        if (!$stream) {
1024
            $stream = new ProductStream();
1025
            $stream->setName($product->stream);
1026
            $stream->setType(ProductStreamService::STATIC_STREAM);
1027
            $stream->setSorting(json_encode(
1028
                [ReleaseDateSorting::class => ['direction' => 'desc']]
1029
            ));
1030
1031
            //add attributes
1032
            $attribute = new \Shopware\Models\Attribute\ProductStream();
1033
            $attribute->setProductStream($stream);
1034
            $attribute->setConnectIsRemote(true);
1035
            $stream->setAttribute($attribute);
1036
1037
            $this->manager->persist($attribute);
1038
            $this->manager->persist($stream);
1039
            $this->manager->flush();
1040
        }
1041
1042
        return $stream;
1043
    }
1044
1045
    /**
1046
     * @param ProductStream $stream
1047
     * @param ProductModel $article
1048
     * @throws \Doctrine\DBAL\DBALException
1049
     */
1050
    private function addProductToStream(ProductStream $stream, ProductModel $article)
1051
    {
1052
        $conn = $this->manager->getConnection();
1053
        $sql = 'INSERT INTO `s_product_streams_selection` (`stream_id`, `article_id`)
1054
                VALUES (:streamId, :articleId)
1055
                ON DUPLICATE KEY UPDATE stream_id = :streamId, article_id = :articleId';
1056
        $stmt = $conn->prepare($sql);
1057
        $stmt->execute([':streamId' => $stream->getId(), ':articleId' => $article->getId()]);
1058
    }
1059
1060
    /**
1061
     * Set detail purchase price with plain SQL
1062
     * Entity usage throws exception when error handlers are disabled
1063
     *
1064
     * @param ProductModel $article
1065
     * @param DetailModel $detail
1066
     * @param Product $product
1067
     * @throws \Doctrine\DBAL\DBALException
1068
     */
1069
    private function setPrice(ProductModel $article, DetailModel $detail, Product $product)
1070
    {
1071
        // set price via plain SQL because shopware throws exception
1072
        // undefined index: key when error handler is disabled
1073
        $customerGroup = $this->helper->getDefaultCustomerGroup();
1074
1075
        if (!empty($product->priceRanges)) {
1076
            $this->setPriceRange($article, $detail, $product->priceRanges, $customerGroup);
1077
1078
            return;
1079
        }
1080
1081
        $id = $this->manager->getConnection()->fetchColumn(
1082
            'SELECT id FROM `s_articles_prices`
1083
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1084
            [$customerGroup->getKey(), 1, 'beliebig', $article->getId(), $detail->getId()]
1085
        );
1086
1087
        // todo@sb: test update prices
1088
        if ($id > 0) {
1089
            $this->manager->getConnection()->executeQuery(
1090
                'UPDATE `s_articles_prices` SET `price` = ?, `baseprice` = ? WHERE `id` = ?',
1091
                [$product->price, $product->purchasePrice, $id]
1092
            );
1093
        } else {
1094
            $this->manager->getConnection()->executeQuery(
1095
                'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`, `baseprice`)
1096
              VALUES (?, 1, "beliebig", ?, ?, ?, ?);',
1097
                [
1098
                    $customerGroup->getKey(),
1099
                    $article->getId(),
1100
                    $detail->getId(),
1101
                    $product->price,
1102
                    $product->purchasePrice
1103
                ]
1104
            );
1105
        }
1106
    }
1107
1108
    /**
1109
     * @param ProductModel $article
1110
     * @param DetailModel $detail
1111
     * @param array $priceRanges
1112
     * @param Group $group
1113
     * @throws \Doctrine\DBAL\ConnectionException
1114
     * @throws \Exception
1115
     */
1116
    private function setPriceRange(ProductModel $article, DetailModel $detail, array $priceRanges, Group $group)
1117
    {
1118
        $this->manager->getConnection()->beginTransaction();
1119
1120
        try {
1121
            // We always delete the prices,
1122
            // because we can not know which record is update
1123
            $this->manager->getConnection()->executeQuery(
1124
                'DELETE FROM `s_articles_prices` WHERE `articleID` = ? AND `articledetailsID` = ?',
1125
                [$article->getId(), $detail->getId()]
1126
            );
1127
1128
            /** @var PriceRange $priceRange */
1129
            foreach ($priceRanges as $priceRange) {
1130
                $priceTo = $priceRange->to == PriceRange::ANY ? 'beliebig' : $priceRange->to;
1131
1132
                //todo: maybe batch insert if possible?
1133
                $this->manager->getConnection()->executeQuery(
1134
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `price`)
1135
                      VALUES (?, ?, ?, ?, ?, ?);',
1136
                    [
1137
                        $group->getKey(),
1138
                        $priceRange->from,
1139
                        $priceTo,
1140
                        $article->getId(),
1141
                        $detail->getId(),
1142
                        $priceRange->price
1143
                    ]
1144
                );
1145
            }
1146
            $this->manager->getConnection()->commit();
1147
        } catch (\Exception $e) {
1148
            $this->manager->getConnection()->rollBack();
1149
            throw new \Exception($e->getMessage());
1150
        }
1151
    }
1152
1153
    /**
1154
     * Set detail purchase price with plain SQL
1155
     * Entity usage throws exception when error handlers are disabled
1156
     *
1157
     * @param DetailModel $detail
1158
     * @param float $purchasePrice
1159
     * @param Group $defaultGroup
1160
     * @throws \Doctrine\DBAL\DBALException
1161
     */
1162
    private function setPurchasePrice(DetailModel $detail, $purchasePrice, Group $defaultGroup)
1163
    {
1164
        if (method_exists($detail, 'setPurchasePrice')) {
1165
            $this->manager->getConnection()->executeQuery(
1166
                'UPDATE `s_articles_details` SET `purchaseprice` = ? WHERE `id` = ?',
1167
                [$purchasePrice, $detail->getId()]
1168
            );
1169
        } else {
1170
            $id = $this->manager->getConnection()->fetchColumn(
1171
                'SELECT id FROM `s_articles_prices`
1172
              WHERE `pricegroup` = ? AND `from` = ? AND `to` = ? AND `articleID` = ? AND `articledetailsID` = ?',
1173
                [$defaultGroup->getKey(), 1, 'beliebig', $detail->getArticleId(), $detail->getId()]
1174
            );
1175
1176
            if ($id > 0) {
1177
                $this->manager->getConnection()->executeQuery(
1178
                    'UPDATE `s_articles_prices` SET `baseprice` = ? WHERE `id` = ?',
1179
                    [$purchasePrice, $id]
1180
                );
1181
            } else {
1182
                $this->manager->getConnection()->executeQuery(
1183
                    'INSERT INTO `s_articles_prices`(`pricegroup`, `from`, `to`, `articleID`, `articledetailsID`, `baseprice`)
1184
              VALUES (?, 1, "beliebig", ?, ?, ?);',
1185
                    [$defaultGroup->getKey(), $detail->getArticleId(), $detail->getId(), $purchasePrice]
1186
                );
1187
            }
1188
        }
1189
    }
1190
1191
    /**
1192
     * Adds translation record for given article
1193
     *
1194
     * @param ProductModel $article
1195
     * @param Product $sdkProduct
1196
     */
1197
    private function addArticleTranslations(ProductModel $article, Product $sdkProduct)
1198
    {
1199
        /** @var \Shopware\Connect\Struct\Translation $translation */
1200
        foreach ($sdkProduct->translations as $key => $translation) {
1201
            /** @var \Shopware\Models\Shop\Locale $locale */
1202
            $locale = $this->getLocaleRepository()->findOneBy(['locale' => LocaleMapper::getShopwareLocale($key)]);
1203
            /** @var \Shopware\Models\Shop\Shop $shop */
1204
            $shop = $this->getShopRepository()->findOneBy(['locale' => $locale]);
1205
            if (!$shop) {
1206
                continue;
1207
            }
1208
1209
            $this->productTranslationsGateway->addArticleTranslation($translation, $article->getId(), $shop->getId());
1210
        }
1211
    }
1212
1213
    /**
1214
     * dsadsa
1215
     * @return \Shopware\Components\Model\ModelRepository
1216
     */
1217
    private function getLocaleRepository()
1218
    {
1219
        if (!$this->localeRepository) {
1220
            $this->localeRepository = $this->manager->getRepository('Shopware\Models\Shop\Locale');
1221
        }
1222
1223
        return $this->localeRepository;
1224
    }
1225
1226
    private function getShopRepository()
1227
    {
1228
        if (!$this->shopRepository) {
1229
            $this->shopRepository = $this->manager->getRepository('Shopware\Models\Shop\Shop');
1230
        }
1231
1232
        return $this->shopRepository;
1233
    }
1234
1235
    /**
1236
     * Delete product or product variant with given shopId and sourceId.
1237
     *
1238
     * Only the combination of both identifies a product uniquely. Do NOT
1239
     * delete products just by their sourceId.
1240
     *
1241
     * You might receive delete requests for products, which are not available
1242
     * in your shop. Just ignore them.
1243
     *
1244
     * @param string $shopId
1245
     * @param string $sourceId
1246
     * @return void
1247
     */
1248
    public function delete($shopId, $sourceId)
1249
    {
1250
        $detail = $this->helper->getArticleDetailModelByProduct(new Product([
1251
            'shopId' => $shopId,
1252
            'sourceId' => $sourceId,
1253
        ]));
1254
        if ($detail === null) {
1255
            return;
1256
        }
1257
1258
        $this->deleteDetail($detail);
1259
    }
1260
1261
    public function update($shopId, $sourceId, ProductUpdate $product)
1262
    {
1263
        // find article detail id
1264
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1265
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1266
            [$sourceId, $shopId]
1267
        );
1268
1269
        $this->eventManager->notify(
1270
            'Connect_Merchant_Update_GeneralProductInformation',
1271
            [
1272
                'subject' => $this,
1273
                'shopId' => $shopId,
1274
                'sourceId' => $sourceId,
1275
                'articleDetailId' => $articleDetailId
1276
            ]
1277
        );
1278
1279
        // update purchasePriceHash, offerValidUntil and purchasePrice in connect attribute
1280
        $this->manager->getConnection()->executeUpdate(
1281
            'UPDATE s_plugin_connect_items SET purchase_price_hash = ?, offer_valid_until = ?, purchase_price = ?
1282
            WHERE source_id = ? AND shop_id = ?',
1283
            [
1284
                $product->purchasePriceHash,
1285
                $product->offerValidUntil,
1286
                $product->purchasePrice,
1287
                $sourceId,
1288
                $shopId,
1289
            ]
1290
        );
1291
1292
        // update stock in article detail
1293
        // update prices
1294
        // if purchase price is stored in article detail
1295
        // update it together with stock
1296
        // since shopware 5.2
1297
        if (method_exists('Shopware\Models\Article\Detail', 'getPurchasePrice')) {
1298
            $this->manager->getConnection()->executeUpdate(
1299
                'UPDATE s_articles_details SET instock = ?, purchaseprice = ? WHERE id = ?',
1300
                [$product->availability, $product->purchasePrice, $articleDetailId]
1301
            );
1302
        } else {
1303
            $this->manager->getConnection()->executeUpdate(
1304
                'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1305
                [$product->availability, $articleDetailId]
1306
            );
1307
        }
1308
        $this->manager->getConnection()->executeUpdate(
1309
            "UPDATE s_articles_prices SET price = ?, baseprice = ? WHERE articledetailsID = ? AND pricegroup = 'EK'",
1310
            [$product->price, $product->purchasePrice, $articleDetailId]
1311
        );
1312
    }
1313
1314
    public function changeAvailability($shopId, $sourceId, $availability)
1315
    {
1316
        // find article detail id
1317
        $articleDetailId = $this->manager->getConnection()->fetchColumn(
1318
            'SELECT article_detail_id FROM s_plugin_connect_items WHERE source_id = ? AND shop_id = ?',
1319
            [$sourceId, $shopId]
1320
        );
1321
1322
        $this->eventManager->notify(
1323
            'Connect_Merchant_Update_GeneralProductInformation',
1324
            [
1325
                'subject' => $this,
1326
                'shopId' => $shopId,
1327
                'sourceId' => $sourceId,
1328
                'articleDetailId' => $articleDetailId
1329
            ]
1330
        );
1331
1332
        // update stock in article detail
1333
        $this->manager->getConnection()->executeUpdate(
1334
            'UPDATE s_articles_details SET instock = ? WHERE id = ?',
1335
            [$availability, $articleDetailId]
1336
        );
1337
    }
1338
1339
    /**
1340
     * @inheritDoc
1341
     */
1342
    public function makeMainVariant($shopId, $sourceId, $groupId)
1343
    {
1344
        //find article detail which should be selected as main one
1345
        $newMainDetail = $this->helper->getConnectArticleDetailModel($sourceId, $shopId);
1346
        if (!$newMainDetail) {
1347
            return;
1348
        }
1349
1350
        /** @var \Shopware\Models\Article\Article $article */
1351
        $article = $newMainDetail->getArticle();
1352
1353
        $this->eventManager->notify(
1354
            'Connect_Merchant_Update_ProductMainVariant_Before',
1355
            [
1356
                'subject' => $this,
1357
                'shopId' => $shopId,
1358
                'sourceId' => $sourceId,
1359
                'articleId' => $article->getId(),
1360
                'articleDetailId' => $newMainDetail->getId()
1361
            ]
1362
        );
1363
1364
        // replace current main detail with new one
1365
        $currentMainDetail = $article->getMainDetail();
1366
        $currentMainDetail->setKind(2);
1367
        $newMainDetail->setKind(1);
1368
        $article->setMainDetail($newMainDetail);
1369
1370
        $this->manager->persist($newMainDetail);
1371
        $this->manager->persist($currentMainDetail);
1372
        $this->manager->persist($article);
1373
        $this->manager->flush();
1374
    }
1375
1376
    /**
1377
     * Updates the status of an Order
1378
     *
1379
     * @param string $localOrderId
1380
     * @param string $orderStatus
1381
     * @param string $trackingNumber
1382
     * @return void
1383
     */
1384
    public function updateOrderStatus($localOrderId, $orderStatus, $trackingNumber)
1385
    {
1386
        if ($this->config->getConfig('updateOrderStatus') == 1) {
1387
            $this->updateDeliveryStatus($localOrderId, $orderStatus);
1388
        }
1389
1390
        if ($trackingNumber) {
1391
            $this->updateTrackingNumber($localOrderId, $trackingNumber);
1392
        }
1393
    }
1394
1395
    /**
1396
     * @param string $localOrderId
1397
     * @param string $orderStatus
1398
     */
1399
    private function updateDeliveryStatus($localOrderId, $orderStatus)
1400
    {
1401
        $status = false;
1402
        if ($orderStatus === OrderStatus::STATE_IN_PROCESS) {
1403
            $status = Status::ORDER_STATE_PARTIALLY_DELIVERED;
1404
        } elseif ($orderStatus === OrderStatus::STATE_DELIVERED) {
1405
            $status = Status::ORDER_STATE_COMPLETELY_DELIVERED;
1406
        }
1407
1408
        if ($status) {
1409
            $this->manager->getConnection()->executeQuery(
1410
                'UPDATE s_order 
1411
                SET status = :orderStatus
1412
                WHERE ordernumber = :orderNumber',
1413
                [
1414
                    ':orderStatus' => $status,
1415
                    ':orderNumber' => $localOrderId
1416
                ]
1417
            );
1418
        }
1419
    }
1420
1421
    /**
1422
     * @param string $localOrderId
1423
     * @param string $trackingNumber
1424
     */
1425
    private function updateTrackingNumber($localOrderId, $trackingNumber)
1426
    {
1427
        $currentTrackingCode = $this->manager->getConnection()->fetchColumn(
1428
            'SELECT trackingcode
1429
            FROM s_order
1430
            WHERE ordernumber = :orderNumber',
1431
            [
1432
                ':orderNumber' => $localOrderId
1433
            ]
1434
        );
1435
1436
        if (!$currentTrackingCode) {
1437
            $newTracking = $trackingNumber;
1438
        } else {
1439
            $newTracking = $this->combineTrackingNumbers($trackingNumber, $currentTrackingCode);
1440
        }
1441
1442
        $this->manager->getConnection()->executeQuery(
1443
            'UPDATE s_order 
1444
            SET trackingcode = :trackingCode
1445
            WHERE ordernumber = :orderNumber',
1446
            [
1447
                ':trackingCode' => $newTracking,
1448
                ':orderNumber' => $localOrderId
1449
            ]
1450
        );
1451
    }
1452
1453
    /**
1454
     * @param string $newTrackingCode
1455
     * @param string $currentTrackingCode
1456
     * @return string
1457
     */
1458
    private function combineTrackingNumbers($newTrackingCode, $currentTrackingCode)
1459
    {
1460
        $currentTrackingCodes = $this->getTrackingNumberAsArray($currentTrackingCode);
1461
        $newTrackingCodes = $this->getTrackingNumberAsArray($newTrackingCode);
1462
        $newTrackingCodes = array_unique(array_merge($currentTrackingCodes, $newTrackingCodes));
1463
        $newTracking = implode(',', $newTrackingCodes);
1464
1465
        return $newTracking;
1466
    }
1467
1468
    /**
1469
     * @param string $trackingCode
1470
     * @return string[]
1471
     */
1472
    private function getTrackingNumberAsArray($trackingCode)
1473
    {
1474
        if (strpos($trackingCode, ',') !== false) {
1475
            return explode(',', $trackingCode);
1476
        }
1477
1478
        return [$trackingCode];
1479
    }
1480
1481
    /**
1482
     * @param Product $product
1483
     * @param ProductModel $model
1484
     */
1485
    private function saveVat(Product $product, ProductModel $model)
1486
    {
1487
        if ($product->vat !== null) {
1488
            $repo = $this->manager->getRepository(Tax::class);
1489
            $taxRate = round($product->vat * 100, 2);
1490
            /** @var \Shopware\Models\Tax\Tax $tax */
1491
            $tax = $repo->findOneBy(['tax' => $taxRate]);
1492
            if (!$tax) {
1493
                $tax = new Tax();
1494
                $tax->setTax($taxRate);
1495
                //this is to get rid of zeroes behind the decimal point
1496
                $name = strval(round($taxRate, 2)) . '%';
1497
                $tax->setName($name);
1498
                $this->manager->persist($tax);
1499
            }
1500
            $model->setTax($tax);
1501
        }
1502
    }
1503
1504
    /**
1505
     * @param Property $property
1506
     * @return PropertyGroup
1507
     */
1508
    private function createPropertyGroup(Property $property)
1509
    {
1510
        $group = new PropertyGroup();
1511
        $group->setName($property->groupName);
1512
        $group->setComparable($property->comparable);
1513
        $group->setSortMode($property->sortMode);
1514
        $group->setPosition($property->groupPosition);
1515
1516
        $attribute = new \Shopware\Models\Attribute\PropertyGroup();
1517
        $attribute->setPropertyGroup($group);
1518
        $attribute->setConnectIsRemote(true);
1519
        $group->setAttribute($attribute);
1520
1521
        $this->manager->persist($attribute);
1522
        $this->manager->persist($group);
1523
        $this->manager->flush();
1524
1525
        return $group;
1526
    }
1527
}
1528