Completed
Pull Request — master (#422)
by Jonas
05:22
created

ProductToShop::setPriceRange()   B

Complexity

Conditions 4
Paths 10

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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