396 lines
13 KiB
PHP
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',
|
|
];
|
|
}
|
|
}
|