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

396 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace External\HannahBundle\SAP\Synchronizer;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use External\HannahBundle\SAP\MappingType;
use External\HannahBundle\Util\Configuration;
use External\HannahBundle\Util\FTP\SFTPClient;
use KupShop\SellerBundle\Utils\SellerUtil;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
class POSOrderSynchronizer extends BaseSynchronizer
{
protected static $type = 'pos_order';
#[Required]
public SellerUtil $sellerUtil;
#[Required]
public SFTPClient $sftp;
private ?string $posFilename = null;
public function setPosFilename(?string $posFilename): void
{
$this->posFilename = $posFilename;
}
public function process(array $data): void
{
foreach ($this->getItems() as $item) {
$this->processItem($item);
if ($this->processItemCallback) {
call_user_func($this->processItemCallback, (array) $item);
}
}
}
protected function processItem(array $item): void
{
if (empty($item['BillId'])) {
return;
}
$saleId = sqlQueryBuilder()
->select('id')
->from('sales')
->where(Operator::equals(['code' => $item['BillId']]))
->sendToMaster()
->execute()->fetchOne();
// prodejka v e-shopu uz existuje
if ($saleId) {
$this->withRetryStrategy(function () use ($saleId, $item) {
sqlGetConnection()->transactional(function () use ($saleId, $item) {
sqlQueryBuilder()
->delete('sales_items')
->where(Operator::equals(['id_sale' => $saleId]))
->execute();
$this->updateSaleHeader($saleId, $item);
$this->updateSaleItems($saleId, $item);
return true;
});
});
return;
}
$sale = [
'id_user' => $this->getUserId($item),
'id_seller' => $this->getSellerId($item),
'id_delivery_type' => $this->getDeliveryTypeId($item),
'code' => $item['BillId'],
'date_created' => $item['Crdate'].' '.$item['Crtime'],
'data' => json_encode([
'sapInvoiceNumber' => $item['SapInvoice'] ?? null,
'sapCustomerId' => $item['CustomerId'] ?? null,
'sapStoreId' => $item['Store'],
]),
];
$this->withRetryStrategy(function () use ($item, $sale) {
sqlGetConnection()->transactional(function () use ($item, $sale) {
sqlQueryBuilder()
->insert('sales')
->directValues($sale)
->execute();
$saleId = (int) sqlInsertId();
$this->updateSaleItems($saleId, $item);
});
return true;
});
}
protected function getSaleHeaderData(array $item): array
{
return [
'id_user' => $this->getUserId($item),
'id_seller' => $this->getSellerId($item),
'id_delivery_type' => $this->getDeliveryTypeId($item),
'code' => $item['BillId'],
'date_created' => $item['Crdate'].' '.$item['Crtime'],
'data' => json_encode([
'sapInvoiceNumber' => $item['SapInvoice'] ?? null,
'sapCustomerId' => $item['CustomerId'] ?? null,
'sapStoreId' => $item['Store'],
]),
];
}
protected function updateSaleHeader(int $saleId, array $item): void
{
sqlQueryBuilder()
->update('sales')
->directValues($this->getSaleHeaderData($item))
->where(Operator::equals(['id' => $saleId]))
->execute();
}
protected function updateSaleItems(int $saleId, array $item): void
{
$totalWithVat = \DecimalConstants::zero();
$totalWithoutVat = \DecimalConstants::zero();
foreach ($item['Items'] ?? [] as $saleItem) {
$productId = null;
$variationId = null;
if ($mapping = $this->sapUtil->getItemMapping($saleItem['Material'])) {
[$productId, $variationId] = $mapping;
}
$totalPriceWithoutVat = toDecimal($saleItem['Netto']);
$totalPriceWithVat = toDecimal($saleItem['Netto'] + $saleItem['Tax']);
if (!$totalPriceWithVat->isZero()) {
$tax = $totalPriceWithVat->div($totalPriceWithoutVat)->sub(\DecimalConstants::one())->mul(\DecimalConstants::hundred())->round(2);
} else {
$tax = \DecimalConstants::zero();
}
$pieces = toDecimal($saleItem['Amount']);
$totalWithoutVat = $totalWithoutVat->add($totalPriceWithoutVat);
$totalWithVat = $totalWithVat->add($totalPriceWithVat);
sqlQueryBuilder()
->insert('sales_items')
->directValues(
[
'id_sale' => $saleId,
'id_product' => $productId,
'id_variation' => $variationId,
'pieces' => $pieces,
'piece_price' => $totalPriceWithoutVat->div($pieces),
'total_price' => $totalPriceWithoutVat,
'tax' => $tax,
'name' => $saleItem['Arktx'],
]
)->execute();
}
sqlQueryBuilder()
->update('sales')
->directValues(['total_price' => $totalWithVat, 'total_price_without_vat' => $totalWithoutVat])
->where(Operator::equals(['id' => $saleId]))
->execute();
}
protected function getDeliveryTypeId(array $item): ?int
{
static $deliveryTypeId;
if ($deliveryTypeId === null) {
$deliveryId = sqlQueryBuilder()
->select('id')
->from('delivery_type_delivery')
->where(Operator::equals(['class' => 'OdberNaProdejne']))
->execute()->fetchOne();
$paymentId = sqlQueryBuilder()
->select('id')
->from('delivery_type_payment')
->where(Operator::equals(['class' => 'Hotovost']))
->execute()->fetchOne();
if ($deliveryId && $paymentId) {
$tmpDeliveryId = sqlQueryBuilder()
->select('id')
->from('delivery_type')
->where(Operator::equals(['id_delivery' => $deliveryId, 'id_payment' => $paymentId]))
->execute()->fetchOne();
$deliveryTypeId = $tmpDeliveryId ?: false;
}
}
return $deliveryTypeId ?: null;
}
protected function getUserId(array $item): ?int
{
if (empty($item['CustomerId'])) {
return null;
}
if (!($userId = $this->sapUtil->getMapping(MappingType::USERS, $item['CustomerId']))) {
if ($this->isStoreUser($item)) {
$userId = $this->createSellerUser($item);
}
}
return $userId;
}
protected function getSellerId(array $item): ?int
{
if (!empty($item['Store'])) {
if ($storeId = $this->sapUtil->getMapping(MappingType::STORES, $item['Store'])) {
$tmpSellerId = sqlQueryBuilder()
->select('id')
->from('sellers')
->where(Operator::equals(['id_store' => $storeId]))
->execute()->fetchOne();
if ($tmpSellerId) {
return $tmpSellerId;
}
}
}
return null;
}
protected function createSellerUser(array $item): ?int
{
if ($sellerId = $this->getSellerId($item)) {
if ($seller = $this->sellerUtil->getSeller($sellerId)) {
$userEmailExists = sqlQueryBuilder()
->select('id')
->from('users')
->where(Operator::equals(['email' => $seller['email']]))
->execute()->fetchOne();
if ($userEmailExists) {
$seller['email'] = null;
}
if (empty($seller['email'])) {
$seller['email'] = "prodejna_{$sellerId}@rockpoint.cz";
}
return sqlGetConnection()->transactional(function () use ($item, $seller) {
sqlQueryBuilder()
->insert('users')
->directValues(
[
'email' => $seller['email'],
'firm' => $seller['title'],
'street' => implode(' ', array_filter([$seller['street'], $seller['number']])),
'city' => $seller['city'],
'zip' => $seller['psc'],
'phone' => $seller['phone'],
'figure' => 'N',
]
)->execute();
$userId = (int) sqlInsertId();
$this->sapUtil->createMapping(MappingType::USERS, $item['CustomerId'], $userId);
return $userId;
});
}
}
return null;
}
protected function isStoreUser(array $item): bool
{
$sapStoreId = ltrim($item['CustomerId'], '0');
return $item['Store'] === $sapStoreId;
}
protected function getItems(): iterable
{
ini_set('memory_limit', '4096M');
foreach ($this->getPOSJsonFiles() as $filename) {
// pokud je nastaveny posFilename, tak chci importovat pouze ten jeden konkretni file
if ($this->posFilename && $this->posFilename !== $filename) {
continue;
}
$tmpFile = $this->sftp->getRemoteJSONFile($filename);
$data = json_decode(
file_get_contents($tmpFile),
true
);
foreach ($data as $item) {
yield $item;
}
if (!isDevelopment()) {
$this->sftp->renameRemoteFile($filename, $filename.'.DONE');
}
unlink($tmpFile);
}
}
protected function getHandledFields(): array
{
return [];
}
private function withRetryStrategy(callable $fn, int $maxTries = 3): mixed
{
$try = 0;
do {
$try++;
try {
return $fn();
} catch (DeadlockException|LockWaitTimeoutException) {
sleep(1);
}
} while ($try < $maxTries);
return false;
}
/**
* Metoda pro získání JSON souborů s datama prodejek.
*/
public function getPOSJsonFiles(): iterable
{
$directory = $this->sftp->getJSONDirectory($this->sftp->isTestEnv());
$files = $this->sftp->getDirectory($directory);
$result = [];
// procházím všechny soubory na FTP
foreach ($files as $file) {
// zajimaji me pouze postdata soubory
if (!preg_match('/^(\S+_)?posdata_(.+).json$/', $file)) {
continue;
}
$parts = explode('_', $file);
// pokud je to soubor, ktery uz obsahuje prefix, tak ho jen ulozim do pole s vysledkama
if (in_array($parts[0], $this->getFilePrefixes())) {
$result[$parts[0]][] = $file;
continue;
}
// pokud je to soubor bez prefixu, tak z neho vytvorim soubor s prefixama, aby to byl soubor per e-shop a byl jsm schopnej to na kazdym shopu snadno zpracovat
$content = $this->sftp->getSFTP()->get($directory.$file);
foreach ($this->getFilePrefixes() as $prefix) {
$this->sftp->getSFTP()->put("{$directory}{$prefix}_{$file}", $content);
$result[$prefix][] = "{$prefix}_{$file}";
}
if (!isDevelopment()) {
$this->sftp->renameRemoteFile($file, $file.'.DONE');
}
}
return $result[$this->getFilePrefixes()[$this->configuration->getShopId()]] ?? [];
}
private function getFilePrefixes(): array
{
return [
Configuration::SHOP_ROCKPOINT => 'RPO',
Configuration::SHOP_HANNAH => 'HAN',
Configuration::SHOP_RAFIKI => 'RAF',
Configuration::SHOP_KEEN => 'KEN',
];
}
}