Files
kupshop/bundles/External/HannahBundle/SAP/Synchronizer/StockSynchronizer.php
2025-08-02 16:30:27 +02:00

238 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace External\HannahBundle\SAP\Synchronizer;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use External\HannahBundle\Enum\ProductAvailability;
use External\HannahBundle\SAP\Exception\SAPException;
use External\HannahBundle\SAP\MappingType;
use External\HannahBundle\Util\Product\ProductAvailabilityUpdater;
use KupShop\StoresBundle\Utils\StoresInStore;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
class StockSynchronizer extends BaseSynchronizer
{
protected static $type = 'disponibility';
protected $logging = false;
#[Required]
public StoresInStore $storesInStore;
#[Required]
public ProductAvailabilityUpdater $productAvailabilityUpdater;
protected function processItem(array $item): void
{
if (empty($item['Matnr'])) {
return;
}
if (!($mapping = $this->sapUtil->getItemMapping($item['Matnr']))) {
// nemam produkt
return;
}
[$productId, $variationId] = $mapping;
$storeId = $this->getStoreId($item);
if (!($sapDateUpdated = \DateTime::createFromFormat('YmdHis', (string) $item['Lastupd']))) {
$sapDateUpdated = null;
}
try {
$this->updateStoreItem(
$storeId,
$productId,
$variationId,
(float) $item['Dispo'],
$sapDateUpdated
);
} catch (ForeignKeyConstraintViolationException $e) {
}
}
protected function getHandledFields(): array
{
return [
'Items' => 'item',
];
}
private function getStoreId(array $item): int
{
static $storeIdCache = [];
$shippingPoint = $item['ShippingPoint'] ?? null;
if (!$shippingPoint) {
throw new SAPException('Shipping point is empty');
}
if ($storeIdCache[$shippingPoint] ?? false) {
return $storeIdCache[$shippingPoint];
}
$storeId = sqlGetConnection()->transactional(function () use ($shippingPoint) {
return $this->sapUtil->getMapping(MappingType::STORES, $shippingPoint);
});
if (!$storeId) {
$storeId = sqlGetConnection()->transactional(function () use ($shippingPoint) {
sqlQueryBuilder()
->insert('stores')
->directValues(
[
'name' => 'Shipping point '.$shippingPoint,
'data' => json_encode(['shippingPoint' => $shippingPoint]),
]
)->execute();
return (int) sqlInsertId();
});
$this->sapUtil->createMapping(MappingType::STORES, $shippingPoint, $storeId);
}
return $storeIdCache[$shippingPoint] = $storeId;
}
public function updateStoreItem(int $storeId, int $productId, ?int $variationId, float $quantity, ?\DateTime $sapDateUpdated = null): void
{
if (empty($productId)) {
return;
}
$isRestocked = false;
if ($storeItem = sqlQueryBuilder()
->select('id, quantity, sap_date_updated')
->from('stores_items')
->andWhere(Operator::equalsNullable(
[
'id_product' => $productId,
'id_variation' => $variationId,
'id_store' => $storeId,
]))
->sendToMaster()
->execute()
->fetchAssociative()) {
if ($sapDateUpdated && ($storeItemSapDateUpdated = \DateTime::createFromFormat('Y-m-d H:i:s', $storeItem['sap_date_updated']))) {
// pokud mam ulozeny spravny casovy razitko ze sapu, ale nesedi mi sklad, tak je invalidStore a budu muset udelat update
$isInvalidStore = $sapDateUpdated == $storeItemSapDateUpdated && $quantity != $storeItem['quantity'];
// pokud mam v databazi ($storeItemSapDateUpdated) ulozenej novejsi zaznam, nez mi aktualne prisel, tak nebudu provat update, protoze
// je to neaktualni skladovost, ktera me nezajima
// zaroven nesmi byt invalidStore, protoze pokud je, tak ten update proste provedu, at se to fixne
if ($sapDateUpdated <= $storeItemSapDateUpdated && !$isInvalidStore) {
// date_updated aktualizuju jen pri plne synchronizaci, protoze jen tehdy me zajima. Jinym zpusobem ho nevyuzivam
if ($this->isFullFileImport()) {
// date_updated budu aktualizovat jen pokud se jedna o full update
sqlQueryBuilder()
->update('stores_items')
->set('date_updated', 'NOW()')
->where(Operator::equals(['id' => $storeItem['id']]))
->execute();
}
return;
}
}
if ($quantity <= 0) {
sqlQueryBuilder()
->delete('stores_items')
->where(Operator::equals(['id' => $storeItem['id']]))
->execute();
$this->sapUtil->recalculateStores([$productId]);
// produkt se vyprodal, takze zmenime jeho dostupnost
$this->productAvailabilityUpdater->updateProductAvailability(
ProductAvailability::UNAVAILABLE,
$productId,
$variationId
);
return;
}
// pokud doslo k znovu naskladneni, tak si to oznacim
if ($storeItem['quantity'] <= 0) {
$isRestocked = true;
}
$updateQb = sqlQueryBuilder()
->update('stores_items')
->where(Operator::equals(['id' => $storeItem['id']]));
$updateData = [
'quantity' => $quantity,
'sap_date_updated' => $sapDateUpdated ? $sapDateUpdated->format('Y-m-d H:i:s') : (new \DateTime())->format('Y-m-d H:i:s'),
];
// pokud se opravdu zmenila quantity, tak nastavim sap_updated na 1
if ($quantity != $storeItem['quantity']) {
$updateData['sap_updated'] = 1;
}
// date_updated aktualizuju jen pri plne synchronizaci, protoze jen tehdy me zajima. Jinym zpusobem ho nevyuzivam
// nebo se opravdu zmenila skladovost
if ($this->isFullFileImport() || $quantity != $storeItem['quantity']) {
$updateQb->set('date_updated', 'NOW()');
}
// nasetuju data a executnu update
$updateQb
->directValues($updateData)
->execute();
} elseif ($quantity > 0) {
sqlQueryBuilder()
->insert('stores_items')
->setValue('date_updated', 'NOW()')
->directValues(
[
'id_store' => $storeId,
'id_product' => $productId,
'id_variation' => $variationId,
'quantity' => $quantity,
'sap_date_updated' => $sapDateUpdated ? $sapDateUpdated->format('Y-m-d H:i:s') : (new \DateTime())->format('Y-m-d H:i:s'),
'sap_updated' => 1,
]
)
->execute();
$isRestocked = true;
}
if ($isRestocked) {
// produkt byl naskladnen, takze provedu update dostupnosti
$this->productAvailabilityUpdater->updateProductAvailability(
ProductAvailability::IN_STORE,
$productId,
$variationId
);
}
}
protected function postprocess(): void
{
// pokud je to full import a je to file import (import z JSON souboru), tak na konci importu spustim vynulovani
// zaznamu, ktere nebyly aktualizovany
if ($this->isFullFileImport()) {
// nastavim 0 vsem polozkam, ktere nebyly aktualizovant vice jak 12 hodin
sqlQueryBuilder()
->update('stores_items')
->directValues(['quantity' => 0])
->where('TIMESTAMPDIFF(HOUR, date_updated, NOW()) > 12')
->execute();
}
}
private function isFullFileImport(): bool
{
return $this->context['importType'] === 'FULL' && ($this->context['isFileImport'] ?? false);
}
}