Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NEXT

- #12748 - Fixed order migration so `product.sales` is recalculated from persisted order line items after migrated orders are written. Cancelled orders are excluded from the sales calculation.

# 18.0.0

- Fixed Shopware 6 sales channel migration so additional sales channels, such as Social Shopping, are now migrated correctly when their sales channel types already exist in the target shop before the migration starts
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG_de-DE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NEXT

- #12748 - Die Migration von Bestellungen wurde korrigiert, sodass `product.sales` nach dem Schreiben migrierter Bestellungen anhand der persistierten Bestellpositionen neu berechnet wird. Stornierte Bestellungen werden dabei nicht berücksichtigt.

# 18.0.0

- #14435 - Die Migration von Shopware 6 Sales Channel wurde korrigiert, sodass zusätzliche Sales Channel wie Social Shopping jetzt korrekt migriert werden, wenn ihre Sales-Channel-Typen vor dem Start der Migration im Zielshop vorhanden sind.
Expand Down
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# NEXT

- [BREAKING] [#178](https://github.com/shopware/SwagMigrationAssistant/pull/190) - fix!: update product.sales during migration
- [BREAKING] Added required constructor parameter `\SwagMigrationAssistant\Migration\Service\ProductSalesUpdater` to `\SwagMigrationAssistant\Migration\Writer\OrderWriter`

# 18.0.0

- [BREAKING] [#178](https://github.com/shopware/SwagMigrationAssistant/pull/178) - fix!: unsupported sales channel migration
Expand Down
6 changes: 6 additions & 0 deletions src/DependencyInjection/shopware.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use SwagMigrationAssistant\Migration\Media\Processor\BaseMediaService;
use SwagMigrationAssistant\Migration\Media\Processor\HttpDownloadServiceBase;
use SwagMigrationAssistant\Migration\MigrationConfiguration;
use SwagMigrationAssistant\Migration\Service\ProductSalesUpdater;
use SwagMigrationAssistant\Migration\Writer\AbstractWriter;
use SwagMigrationAssistant\Profile\Shopware\Converter\AttributeConverter;
use SwagMigrationAssistant\Profile\Shopware\Converter\CategoryAttributeConverter;
Expand Down Expand Up @@ -997,4 +998,9 @@
service(PromotionDefinition::class),
])
->tag('shopware.migration.writer');

$services->set(ProductSalesUpdater::class)
->args([
service(Connection::class),
]);
};
2 changes: 2 additions & 0 deletions src/DependencyInjection/writer.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use Shopware\Core\System\NumberRange\NumberRangeDefinition;
use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
use SwagMigrationAssistant\Migration\ErrorResolution\MigrationErrorResolutionService;
use SwagMigrationAssistant\Migration\Service\ProductSalesUpdater;
use SwagMigrationAssistant\Migration\Writer\AbstractWriter;
use SwagMigrationAssistant\Migration\Writer\CategoryAttributeWriter;
use SwagMigrationAssistant\Migration\Writer\CategoryWriter;
Expand Down Expand Up @@ -162,6 +163,7 @@
service(EntityWriter::class),
service(OrderDefinition::class),
service(StructNormalizer::class),
service(ProductSalesUpdater::class),
])
->tag('shopware.migration.writer');

Expand Down
156 changes: 156 additions & 0 deletions src/Migration/Service/ProductSalesUpdater.php
Comment thread
DennisGarding marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace SwagMigrationAssistant\Migration\Service;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates;
use Shopware\Core\Checkout\Order\OrderStates;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Uuid\Uuid;

#[Package('fundamentals@after-sales')]
final readonly class ProductSalesUpdater
{
public function __construct(
private Connection $connection,
) {
}

/**
* @param array<string> $orderIds
*
* @return array<string>
*/
public function getProductIdsForOrders(array $orderIds): array
{
$orderIds = $this->filterIds($orderIds);

if ($orderIds === []) {
return [];
}

$sql = <<<'SQL'
SELECT DISTINCT order_line_item.product_id
FROM order_line_item
WHERE order_line_item.order_id IN (:orderIds)
AND order_line_item.version_id = :liveVersion
AND order_line_item.order_version_id = :liveVersion
AND order_line_item.product_version_id = :liveVersion
AND order_line_item.type = :lineItemType
AND order_line_item.product_id IS NOT NULL
SQL;

$productIds = $this->connection->fetchFirstColumn(
$sql,
[
'orderIds' => Uuid::fromHexToBytesList($orderIds),
'liveVersion' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
'lineItemType' => LineItem::PRODUCT_LINE_ITEM_TYPE,
],
[
'orderIds' => ArrayParameterType::BINARY,
]
);

return \array_map(
static fn (string $productId): string => Uuid::fromBytesToHex($productId),
$productIds
);
}

/**
* Recalculates the denormalized product.sales value from persisted live order line items.
*
* @param array<string> $productIds
*/
public function updateProducts(array $productIds): void
{
$productIds = $this->filterIds($productIds);

if ($productIds === []) {
return;
}

$productByteIds = Uuid::fromHexToBytesList($productIds);

$parameters = [
'productIds' => $productByteIds,
'outerProductIds' => $productByteIds,
'liveVersion' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
'lineItemType' => LineItem::PRODUCT_LINE_ITEM_TYPE,
'orderStates' => [
OrderStates::STATE_OPEN,
OrderStates::STATE_COMPLETED,
],
'transactionStates' => [
OrderTransactionStates::STATE_CANCELLED,
OrderTransactionStates::STATE_REFUNDED,
],
];

$types = [
'productIds' => ArrayParameterType::BINARY,
'outerProductIds' => ArrayParameterType::BINARY,
'orderStates' => ArrayParameterType::STRING,
'transactionStates' => ArrayParameterType::STRING,
];

$sql = <<<'SQL'
UPDATE product
LEFT JOIN (
SELECT order_line_item.product_id, SUM(order_line_item.quantity) AS sales
FROM order_line_item
INNER JOIN `order`
ON `order`.id = order_line_item.order_id
AND `order`.version_id = order_line_item.order_version_id
AND `order`.version_id = :liveVersion
INNER JOIN state_machine_state order_state
ON order_state.id = `order`.state_id
INNER JOIN order_transaction primary_transaction
ON primary_transaction.id = `order`.primary_order_transaction_id
AND primary_transaction.version_id = `order`.primary_order_transaction_version_id
AND primary_transaction.order_id = `order`.id
AND primary_transaction.order_version_id = `order`.version_id
INNER JOIN state_machine_state transaction_state
ON transaction_state.id = primary_transaction.state_id
WHERE order_line_item.product_id IN (:productIds)
AND order_line_item.version_id = :liveVersion
AND order_line_item.product_version_id = :liveVersion
AND order_line_item.type = :lineItemType
AND order_state.technical_name IN (:orderStates)
AND transaction_state.technical_name NOT IN (:transactionStates)
GROUP BY order_line_item.product_id
) product_sales
ON product_sales.product_id = product.id
SET product.sales = COALESCE(product_sales.sales, 0),
product.updated_at = NOW()
WHERE product.id IN (:outerProductIds)
AND product.version_id = :liveVersion
SQL;

RetryableQuery::retryable($this->connection, function () use ($sql, $parameters, $types): void {
$this->connection->executeStatement($sql, $parameters, $types);
});
}

/**
* @param array<string> $ids
*
* @return array<string>
*/
private function filterIds(array $ids): array
{
$filteredIds = \array_filter($ids, static fn (string $id): bool => Uuid::isValid($id));

return \array_unique($filteredIds);
}
}
93 changes: 92 additions & 1 deletion src/Migration/Writer/OrderWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

namespace SwagMigrationAssistant\Migration\Writer;

use Shopware\Core\Checkout\Order\OrderDefinition;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriterInterface;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Struct\Serializer\StructNormalizer;
use SwagMigrationAssistant\Migration\DataSelection\DefaultEntities;
use SwagMigrationAssistant\Migration\Service\ProductSalesUpdater;

#[Package('fundamentals@after-sales')]
class OrderWriter extends AbstractWriter
Expand All @@ -21,6 +24,7 @@ public function __construct(
EntityWriterInterface $entityWriter,
EntityDefinition $definition,
private readonly StructNormalizer $structNormalizer,
private readonly ProductSalesUpdater $productSalesUpdater,
Comment thread
DennisGarding marked this conversation as resolved.
) {
parent::__construct($entityWriter, $definition);
}
Expand All @@ -32,6 +36,8 @@ public function supports(): string

public function writeData(array $data, Context $context): array
{
// The migration payload still contains serialized transaction amounts.
// Convert them back to price structs before the DAL writes the orders.
foreach ($data as &$item) {
if (!isset($item['transactions']) || !\is_array($item['transactions'])) {
continue;
Expand All @@ -44,6 +50,91 @@ public function writeData(array $data, Context $context): array
}
unset($item);

return parent::writeData($data, $context);
// Read the current products for the target orders before the upsert.
// In a re-migration, a line item can switch from product A to product B,
// so product A would no longer be reachable after the write.
$orderIdsBeforeWrite = $this->extractOrderIdsFromPayload($data);
$productIdsBeforeWrite = $this->productSalesUpdater->getProductIdsForOrders($orderIdsBeforeWrite);

// Let the regular writer create new orders or update already migrated orders.
$result = parent::writeData($data, $context);

// Collect all written order IDs. The payload IDs cover existing orders,
// while the DAL write result also confirms newly inserted orders.
$writtenOrderIds = $this->extractOrderIdsFromWriteResults($result);
$orderIds = $this->merge($orderIdsBeforeWrite, $writtenOrderIds);

// Read the products again after the write to catch new or changed line items.
$productIdsAfterWrite = $this->productSalesUpdater->getProductIdsForOrders($orderIds);
$affectedProductIds = $this->merge($productIdsBeforeWrite, $productIdsAfterWrite);

// Recalculate the final sales value for every affected product.
// The updater derives the value from persisted line items, so repeated
// migration runs replace the sales value instead of incrementing it again.
$this->productSalesUpdater->updateProducts($affectedProductIds);

return $result;
}

/**
* Reads the target order IDs from the converted migration payload before the DAL upsert.
* These IDs are needed to find products that may disappear from an existing order during re-migration.
*
* @param array<array-key, array{id?: string|null}> $payload
*
* @return list<string>
*/
private function extractOrderIdsFromPayload(array $payload): array
{
$orderIds = [];

foreach ($payload as $item) {
$id = $item['id'] ?? null;

if (\is_string($id)) {
$orderIds[] = $id;
}
}

return \array_values(\array_unique($orderIds));
}

/**
* @param array<string, array<EntityWriteResult>> $writeResults
*
* @return list<string>
*/
private function extractOrderIdsFromWriteResults(array $writeResults): array
{
$orderIds = [];

foreach ($writeResults[OrderDefinition::ENTITY_NAME] ?? [] as $writeResult) {
$primaryKey = $writeResult->getPrimaryKey();

if (\is_string($primaryKey)) {
$orderIds[] = $primaryKey;

continue;
}

$id = $primaryKey['id'] ?? null;

if (\is_string($id)) {
$orderIds[] = $id;
}
}

return \array_values(\array_unique($orderIds));
}

/**
* @param array<string> $arrayOne
* @param array<string> $arrayTwo
*
* @return list<string>
*/
private function merge(array $arrayOne, array $arrayTwo): array
{
return \array_values(\array_unique(\array_merge($arrayOne, $arrayTwo)));
}
}
Loading
Loading