first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,542 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Api;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderDiscountBundle\Entity\OrderDiscount;
use KupShop\OrderDiscountBundle\Triggers\CouponTrigger;
use KupShop\OrderDiscountBundle\Triggers\DateTimeTrigger;
use KupShop\OrderDiscountBundle\Triggers\GeneratedCouponTrigger;
use KupShop\OrderDiscountBundle\Triggers\UsesCountTrigger;
use KupShop\OrderDiscountBundle\Util\DiscountManager;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\OrderingBundle\Exception\CartValidationException;
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
use Query\Operator;
use Query\QueryBuilder;
class ApiUtil
{
private ActivityLog $activityLog;
private PurchaseUtil $purchaseUtil;
private ProductList $productList;
private DiscountManager $discountManager;
private ContextManager $contextManager;
public function __construct(
ActivityLog $activityLog,
PurchaseUtil $purchaseUtil,
ProductList $productList,
DiscountManager $discountManager,
ContextManager $contextManager,
) {
$this->activityLog = $activityLog;
$this->purchaseUtil = $purchaseUtil;
$this->productList = $productList;
$this->discountManager = $discountManager;
$this->contextManager = $contextManager;
}
public function auth(array $data): ?array
{
$username = $data['user'] ?? null;
$password = $data['password'] ?? '';
$admin = sqlQueryBuilder()
->select('id, password, token')
->from('admins')
->where(Operator::equals(['login' => $username]))
->execute()->fetchAssociative();
if (!$admin) {
return null;
}
if (!password_verify((string) $password, $admin['password'])) {
return null;
}
return [
'value' => 'Bearer '.$admin['token'],
'expiresIn' => $this->formatDateTime((new \DateTime())->add(new \DateInterval('P1M'))),
];
}
/**
* Kontrola / uplatnění existujícího voucheru. Backward compatibility s API od Air&Me od kterého přecházeli - je to kvůli tomu, aby se
* nemuselo měnit nic na straně pokladen.
*/
public function useVoucher(array $data, bool $try = false, bool $get = false): array
{
/** @var CurrencyContext $currencyContext */
$currencyContext = Contexts::get(CurrencyContext::class);
$currency = $data['localcurrencyisocode'] ?? null;
if (!array_key_exists($currency, $currencyContext->getAll())) {
$currency = $currencyContext->getDefaultId();
}
$coupon = $data['number'] ?? null;
$message = '[PompoAPI] Volání metody "UseVoucher", try='.((int) $try).', kupón: %s';
if ($get) {
$message = '[PompoAPI] Volání metody "GetVoucher", kupón: %s';
}
// zalogovat volani
$this->activityLog->addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_SYNC,
sprintf($message, $coupon),
[
'data' => $data,
]
);
// aktivuju spravnou currency
[$orderDiscount, $result] = $this->contextManager->activateContexts([CurrencyContext::class => $currency], function () use ($coupon, $data) {
$purchaseState = $this->createPurchaseStateFromProductsInfo($data['productsinfo'] ?? []);
$purchaseState->addCoupon($coupon);
$this->discountManager->setPurchaseState($purchaseState);
$this->discountManager->recalculate();
return $this->getUseVoucherData($purchaseState, $coupon);
});
// uplatnuji kupon
if (!$try && $result['isValid'] && $result['isUsable']) {
// pridavam pocet pouziti ke sleve
sqlQueryBuilder()
->update('order_discounts')
->set('uses_count', 'uses_count + 1')
->where(Operator::equals(['id' => $orderDiscount->getId()]))
->execute();
if ($generatedCouponTrigger = $this->getTriggerByType($orderDiscount->getTriggers(), GeneratedCouponTrigger::getType())) {
// hledam prodejnu, na ktery byl uplatnen
$sellerId = null;
if (!empty($data['locationnumber'])) {
$sellerId = $this->getSellerByBranchId((string) $data['locationnumber']);
}
// ulozim si nejake informace o pouziti kuponu
// primarne jde o prodejnu, na ktere byl kupon pouzit
$usageInfo = [
'sellerId' => $sellerId,
'date' => (new \DateTime())->format('Y-m-d H:i:s'),
'metadata' => $data['operationmetadata'] ?? '',
'apiResult' => $result,
];
sqlQueryBuilder()
->update('discounts_coupons')
->directValues(
[
'used' => 'Y',
'data' => json_encode($usageInfo),
]
)
->where(
Operator::equals(
[
'code' => $coupon,
]
)
)->execute();
// zalogovat uplatneni poukazu
$this->activityLog->addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_SYNC,
sprintf('[PompoAPI] Poukaz "%s" byl úspěšně uplatněn.', $coupon),
[
'sellerId' => $sellerId,
'apiResult' => $result,
]
);
}
}
return $result;
}
/**
* Reaktivuje kupón - změní stav z Uplatněno na Neuplatněno.
*/
public function rechargeVoucher(array $data): bool
{
$couponNumber = $data['number'] ?? null;
$coupon = sqlQueryBuilder()
->select('*')
->from('discounts_coupons')
->where(Operator::equals(['code' => $couponNumber]))
->execute()->fetchAssociative();
if (!$coupon) {
return false;
}
if ($coupon['used'] != 'Y') {
return false;
}
$this->activityLog->addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_SYNC,
sprintf('[PompoAPI] Volání metody "RechargeVoucher", kupón: %s', $couponNumber),
[
'data' => $data,
]
);
sqlQueryBuilder()
->update('discounts_coupons')
->directValues(['used' => 'N'])
->where(Operator::equals(['id' => $coupon['id']]))
->execute();
return true;
}
/**
* Backward compatibility s API od Air&Me od kterého přecházeli. Vrací to obecné data o voucheru - na které produktu lze uplatnit, zda
* existuje, zda se dá použít...
*/
private function getUseVoucherData(PurchaseState $purchaseState, string $coupon): array
{
// zda kupon existuje a je mozne ho uplatnit
$isValid = false;
$message = null;
// usability status - pozustatek z Air&me / Pokusil jsem se ho zjednodusit na dva stavy, protoze to nechceme rozlisovat
$usabilityStatus = 0;
// kontroluju validitu kuponu
if ($this->discountManager->couponNumberExists($coupon, $orderDiscounts)) {
try {
if ($this->discountManager->isCouponValid($coupon, $orderDiscounts)) {
$isValid = true;
$usabilityStatus = 1;
}
} catch (CartValidationException $e) {
$message = $e->getMessage();
}
}
$discountId = array_keys($orderDiscounts ?? [])[0] ?? null;
// nactu si produkty, na ktere byla sleva uplatnena
$usableForProducts = [];
$purchaseStateProducts = array_filter($purchaseState->getProducts(), function ($x) {
return strpos($x->getNote()['description'] ?? '', 'tmp product') === false;
});
foreach ($purchaseStateProducts as $item) {
$productDiscounts = array_filter($item->getDiscounts(), function ($x) use ($discountId) { return $x['id'] == $discountId; });
if (empty($productDiscounts)) {
continue;
}
$usableForProducts[] = $item->getProduct()->code;
}
// pokud z pokladny prijde produkt, kterej neni na shopu, tak je v purchase statu tmp product, abych mohl spocitat slevu
// zaroven potrebuju vratit, pro ktery produkty je ta sleva pouzitelna, takze v pripade, ze mam tmp product vratim vsechny produkty
// ktere mi prisly
if ($this->purchaseStateHasTmpItem($purchaseState)) {
$usableForProducts = array_merge(
$purchaseState->getCustomData('productNumbers') ?: [],
$usableForProducts
);
}
// zda byl kupon pouzit
$isUsable = $isValid && (!empty($usableForProducts) || empty($purchaseStateProducts)) && $discountId && in_array($discountId, $purchaseState->getUsedDiscounts() ?? []);
$orderDiscount = null;
if ($discountId) {
$orderDiscount = $this->discountManager->getOrderDiscountById($discountId);
}
$resultData = [
'isValid' => $isValid,
'isUsable' => $isUsable,
'usabilityStatus' => $usabilityStatus,
'message' => $message,
'voucher' => ($isUsable && $discountId) ? $this->getCouponData($purchaseState, $orderDiscount, $coupon) : null,
'usableForProducts' => $usableForProducts,
];
return [$orderDiscount, $resultData];
}
/**
* Backward compatibility s API od Air&Me od kterého přecházeli. Vrací to informace o konkrétním voucheru - platnost, hodnotu atd...
*/
private function getCouponData(PurchaseState $purchaseState, OrderDiscount $orderDiscount, string $coupon): array
{
$discountItem = array_filter($purchaseState->getDiscounts(), function ($x) use ($orderDiscount) { return $x->getIdDiscount() == $orderDiscount->getId(); });
$discountItem = reset($discountItem);
try {
$date = new \DateTime($orderDiscount->getDateCreated());
} catch (\Throwable $e) {
$date = new \DateTime();
}
// One-time
$useType = 1;
$discountType = 'value';
foreach ($orderDiscount->getActions() as $action) {
if (($action['data']['unit'] ?? false) === 'perc') {
$discountType = 'perc';
break;
}
}
if (!($trigger = $this->getTriggerByType($orderDiscount->getTriggers(), CouponTrigger::getType()))) {
$trigger = $this->getTriggerByType($orderDiscount->getTriggers(), GeneratedCouponTrigger::getType());
}
$dateFrom = (new \DateTime())->setTime(0, 0);
$dateTo = (new \DateTime())->add(new \DateInterval('P1Y'));
// je to generated coupon
if ($trigger['data']['generate_coupon'] ?? false) {
if ($generatedCoupon = $this->getGeneratedCoupon((int) $trigger['data']['generate_coupon'])) {
if ($generatedCoupon['date_from'] ?? false) {
$dateFrom = $this->createDateTimeFromString($generatedCoupon['date_from'] ?: '');
}
if ($generatedCoupon['date_to'] ?? false) {
$dateTo = $this->createDateTimeFromString($generatedCoupon['date_to'] ?: '');
}
}
} else {
// Repeatedly
$useType = 2;
// pokud to neni generovany kod a ma use count trigger
if ($this->getTriggerByType($orderDiscount->getTriggers(), UsesCountTrigger::getType())) {
// RepeatedlyCountDecreasing
$useType = 4;
}
}
// zkusim najit date interval trigger
if ($dateTimeTrigger = $this->getTriggerByType($orderDiscount->getTriggers(), DateTimeTrigger::getType())) {
if ($dateTimeTrigger['data']['dateFrom'] ?? false) {
$dateFrom = $this->createDateTimeFromString($dateTimeTrigger['data']['dateFrom'] ?: '');
}
if ($dateTimeTrigger['data']['dateTo'] ?? false) {
$dateTo = $this->createDateTimeFromString($dateTimeTrigger['data']['dateTo'] ?: '');
}
}
$discountValue = $discountItem ? $discountItem->getPriceWithVat()->abs()->asFloat() : 0;
$discountValueType = 0;
// pokud je to precentuelni sleva, tak si procenta vytahnu z nazvu slevy
if ($discountType === 'perc') {
if ($discountItem && preg_match('/\((?<discount>\d+)%\)$/', $discountItem->getName(), $match)) {
$discountValue = (float) $match['discount'];
} else {
$discountValue = 0;
}
$discountValueType = 1;
}
return [
'voucherID' => (int) $trigger['id'],
'templateID' => (int) $orderDiscount->getId(),
'number' => $coupon,
'shortNumber' => '',
'userID' => 0,
'value' => $discountValue,
'status' => 1,
'created' => $this->formatDateTime($date),
'modified' => $this->formatDateTime($date),
'template' => [
'voucherTemplateId' => (int) $orderDiscount->getId(),
'name' => $orderDiscount->getDisplayName(),
'description' => $orderDiscount->getDisplayName(),
'valueType' => $discountValueType,
'useType' => $useType,
'value' => $discountValue,
'isActive' => $orderDiscount->isActive() === 'Y',
'numberMaskId' => (int) $orderDiscount->getId(),
'numberMask' => '',
'shortNumberMaskId' => (int) $orderDiscount->getId(),
'shortNumberMask' => '',
'discountRelativeValueType' => 0,
'validityRelative' => 0,
'validityFixed' => $this->formatDateTime($dateTo),
'created' => $this->formatDateTime($date),
'updateTime' => $this->formatDateTime($date),
'categoryCodes' => [],
],
'attributes' => [],
'detailURL' => null,
'validSince' => $this->formatDateTime($dateFrom),
'validTill' => $this->formatDateTime($dateTo),
];
}
private function formatDateTime(\DateTime $dateTime): string
{
return $dateTime->format('Y-m-d\TH:i:s');
}
private function createDateTimeFromString(string $datetime): \DateTime
{
$defaultDateTime = (new \DateTime())->setTime(0, 0, 0);
if (empty($datetime) || $datetime === '0000-00-00 00:00:00') {
return $defaultDateTime;
}
try {
return new \DateTime($datetime);
} catch (\Throwable $e) {
return $defaultDateTime;
}
}
private function getGeneratedCoupon(int $id): ?array
{
$coupon = sqlQueryBuilder()
->select('*')
->from('discounts_coupons')
->where(Operator::equals(['id' => $id]))
->execute()->fetchAssociative();
if (!$coupon) {
return null;
}
return $coupon;
}
private function getTriggerByType(array $triggers, string $type): ?array
{
$trigger = array_filter($triggers, function ($x) use ($type) { return $x['type'] === $type; });
if (!($trigger = reset($trigger))) {
return null;
}
return $trigger;
}
/**
* Vytvori mi PurchaseState z produktů, které jsou poslány do API.
*/
private function createPurchaseStateFromProductsInfo(array $items): PurchaseState
{
$purchaseState = new PurchaseState([]);
$productNumbers = array_map(function ($x) { return $x['productnumber']; }, $items);
$this->productList->andSpec(function (QueryBuilder $qb) use ($productNumbers) {
return Operator::inStringArray($productNumbers, 'p.code');
});
$products = Mapping::mapKeys($this->productList->getProducts()->toArray(), function ($k, $v) {
return [$v->code, $v];
});
$currency = Contexts::get(CurrencyContext::class)->getActive();
foreach ($items as $item) {
$price = toDecimal($item['totalprice']);
if ($price->isZero()) {
$price = \DecimalConstants::one();
}
$price = $price->div(toDecimal($item['quantity']));
/** @var \Product $product */
if (!($product = ($products[$item['productnumber']] ?? false))) {
$purchaseState->addProduct(
$this->getTmpProductPurchaseItem(
$price,
(float) $item['quantity']
)
);
continue;
}
$purchaseState->addProduct(
new ProductPurchaseItem(
$product->id,
$product->variationId ?? null,
$item['quantity'],
new Price($price, $currency, 0)
)
);
}
// hack pro prazdnej purchasestate
if (empty($purchaseState->getProducts())) {
$purchaseState->addProduct(
$this->getTmpProductPurchaseItem(
toDecimal(999999),
1
)
);
}
$purchaseState->setCustomData([
'isPompoApi' => true,
'productNumbers' => $productNumbers,
]);
return $this->purchaseUtil->recalculateTotalPrices($purchaseState);
}
private function getSellerByBranchId(string $branchId): ?int
{
$sellerId = sqlQueryBuilder()
->select('id')
->from('sellers')
->where(
Operator::equals(['JSON_VALUE(data, \'$.branchId\')' => $branchId])
)
->execute()->fetchOne();
if (!$sellerId) {
return null;
}
return (int) $sellerId;
}
private function getTmpProductPurchaseItem(\Decimal $price, float $pieces): ProductPurchaseItem
{
static $tmpCounter = 1;
return new ProductPurchaseItem(
sqlQuery('SELECT id FROM products LIMIT 1')->fetchOne(),
null,
$pieces,
new Price($price, Contexts::get(CurrencyContext::class)->getActive(), 0),
['description' => 'tmp product ('.$tmpCounter++.')']
);
}
private function purchaseStateHasTmpItem(PurchaseState $purchaseState): bool
{
$result = false;
foreach ($purchaseState->getProducts() as $item) {
if (strpos($item->getNote()['description'] ?? '', 'tmp product') !== false) {
$result = true;
break;
}
}
return $result;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\AutomaticImport\KnizniWeb;
use External\PompoBundle\Exception\PompoException;
use KupShop\KupShopBundle\Util\System\CurlUtil;
use Symfony\Component\HttpClient\CurlHttpClient;
class KnizniWebAPI
{
private const API_BASE_URL = 'https://vo.knizniweb.cz/b2bGate/v2';
private ?CurlHttpClient $client = null;
public function __construct(
private readonly CurlUtil $curlUtil,
) {
}
public function getFeedZipFile(string $token): ?array
{
return $this->request(['token' => $token]) ?? null;
}
public function requestNewFeed(KnizniWebTypeEnum $type): array
{
return $this->request(['synctype' => $type->value]);
}
private function request(array $params): array
{
$response = $this->getClient()->request(
'GET',
$this->getAPIUrl($params)
);
if ($response->getStatusCode() >= 400) {
throw new PompoException('KnizniWeb: api request failure!', null, [
'statusCode' => $response->getStatusCode(),
'data' => $response->getContent(false),
], $response->getStatusCode());
}
return json_decode($response->getContent() ?: '', true) ?: [];
}
private function getAPIUrl(array $params): string
{
return self::API_BASE_URL.'?'.http_build_query(array_merge($this->getCredentials(), $params));
}
private function getCredentials(): array
{
return [
'login' => 'jakub.sula@pompo.cz',
'password' => 'Pompo@123',
];
}
private function getClient(): CurlHttpClient
{
if (!$this->client) {
$this->client = $this->curlUtil->getClient();
}
return $this->client;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace External\PompoBundle\Util\AutomaticImport\KnizniWeb;
enum KnizniWebTypeEnum: string
{
case TITULY = 'T';
case ANOTACE = 'A';
case SKLAD = 'S';
case CISELNIKY = 'C';
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\AutomaticImport\KnizniWeb;
use External\PompoBundle\Exception\PompoException;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Util\FileUtil;
use KupShop\KupShopBundle\Util\System\PathFinder;
class KnizniWebUtil
{
public function __construct(
private KnizniWebAPI $api,
private PathFinder $pathFinder,
private ActivityLog $activityLog,
) {
}
public function requestNewFeed(KnizniWebTypeEnum $type): ?string
{
$dbcfg = \Settings::getDefault();
$result = $this->api->requestNewFeed($type);
$this->addActivityLog('Byla odeslána žádost o nový feed: '.$type->name, [
'type' => $type->value,
'result' => $result,
]);
// ulozim si token pro dalsi feed
if (!empty($result['token'])) {
$config = $dbcfg->loadValue('knizniweb') ?: [];
$config['tokens'][$type->value] = $result['token'];
$dbcfg->saveValue('knizniweb', $config, false);
}
return !empty($result['token']) ? $result['token'] : null;
}
public function getFeedFile(KnizniWebTypeEnum $type, string $token, bool $withNewFeedRequest = true): ?string
{
$delay = false;
if ($withNewFeedRequest) {
$token = $this->requestNewFeed($type);
$delay = true;
}
try {
$file = $this->api->getFeedZipFile($token);
} catch (PompoException $e) {
if ($e->getCode() == '403' || $e->getCode() == '410') {
return null;
} else {
throw $e;
}
}
if (!$file || ($file['status'] ?? null) != 200) {
$delay = true;
}
if ($delay) {
$this->addActivityLog('Čekám na vygenerování XML: '.$type->name, [
'type' => $type->value,
'token' => $token,
]);
$try = 1;
do {
sleep(60);
$file = $this->api->getFeedZipFile($token);
} while ($try++ < 6);
$this->addActivityLog('Vygenerované XML - '.($file ? 'success' : 'timeout').': '.$type->name, [
'type' => $type->value,
'token' => $token,
'file' => $file,
]);
}
$this->addActivityLog('Byly získány infromace o vygenerovaném feedu: '.$type->name, [
'type' => $type->value,
'token' => $token,
'file' => $file,
]);
if (empty($file['url'])) {
return null;
}
$this->addActivityLog('Stahuji feed pro import: '.$type->name.' - '.$file['url']);
$result = $this->zipDownloadAndUnzip($file['url'], $this->getFeedFolder($token))[0] ?? null;
$this->addActivityLog('Byl stažen feed pro import: '.$type->name.' - '.$file['url'], [
'result' => $result,
]);
return $result;
}
public function cleanup(): array
{
$freshCache = [];
foreach (scandir($this->pathFinder->getTmpDir()) as $file) {
if (!str_starts_with($file, 'knizniweb')) {
continue;
}
$filePath = $this->pathFinder->tmpPath($file);
// pokud je to jeste fresh
if ((time() - filemtime($filePath)) <= 1800) {
$token = str_replace('knizniweb_', '', $file);
$xmlFile = array_values(array_map(
fn ($x) => rtrim($filePath, '/').'/'.$x,
array_filter(scandir($filePath), fn ($x) => !empty($x) && !in_array($x, ['.', '..']))
));
$freshCache[$token] = $xmlFile[0] ?? false;
continue;
}
FileUtil::deleteDir($filePath);
}
return $freshCache;
}
private function getFeedFolder(string $token): string
{
return $this->pathFinder->tmpPath("knizniweb_{$token}");
}
private function zipDownloadAndUnzip(string $url, string $destination): array
{
$downloader = new \Downloader();
$downloader->setMethod('curl');
$tmpZip = $this->pathFinder->tmpPath('knizniweb.zip');
// stahnu zip soubor
$downloader->copyRemoteFile($url, $tmpZip);
if (!file_exists($destination)) {
mkdir($destination);
}
// unzipnu soubor do slozky
$files = FileUtil::unzip($tmpZip, $destination);
// smazu zip
unlink($tmpZip);
// vratim obsah slozky, kam jsem rozbalil zip
return $files;
}
private function addActivityLog(string $message, array $data = [], string $severity = ActivityLog::SEVERITY_NOTICE): void
{
$this->activityLog->addActivityLog(
$severity,
ActivityLog::TYPE_IMPORT,
'KnizniWeb: '.$message,
$data
);
}
}

View File

@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use Query\Operator;
class Configuration
{
public const SHOP_POMPO = 'pompo';
public const SHOP_NEJHRACKA = 'nejhracka';
private array $configuration = [];
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
public function has(string $name): bool
{
return isset($this->configuration[$name]);
}
public function get(string $name, $default = null)
{
if (!$this->has($name)) {
return $default;
}
return $this->configuration[$name];
}
public function getShop(): string
{
return $this->get('shop');
}
public function getOrderFinalStatus(): int
{
return (int) $this->get('order.final_status');
}
public function isPompo(): bool
{
return $this->getShop() === self::SHOP_POMPO;
}
public function isNejhracka(): bool
{
return $this->getShop() === self::SHOP_NEJHRACKA;
}
public function getMainStoreId(): int
{
return $this->get('store.id');
}
public function getDefaultSupplierId(): array
{
return $this->get('supplier.defaults', []);
}
public function getMarketplaceSuppliers(): array
{
if (!findModule(\Modules::PRODUCTS_SUPPLIERS)) {
return [];
}
static $suppliersCache;
if ($suppliersCache === null) {
$qb = sqlQueryBuilder()
->select('id, name, data')
->from('suppliers')
->where(Operator::not(Operator::inIntArray($this->getDefaultSupplierId(), 'id')));
$suppliersCache = [];
foreach ($qb->execute() as $item) {
$item['data'] = json_decode($item['data'] ?: '', true) ?: [];
$suppliersCache[$item['id']] = $item;
}
}
return $suppliersCache;
}
/**
* Vrátí pole [MENA => ID_CENIKU], ktere rika jaky cenik plati pro jakou menu.
*/
public function getPriceLists(): array
{
return [
'EUR' => 1,
];
}
public function getUsersPriceLists(): array
{
if ($this->isNejhracka()) {
return [];
}
return [
'CZK' => 2,
'EUR' => 3,
];
}
public function getDMOCPriceLists(): array
{
if ($this->isNejhracka()) {
return [
'CZK' => 2,
'EUR' => 3,
'RON' => 2,
];
}
return [
'CZK' => 5,
'EUR' => 6,
];
}
/**
* Cenik s cenou, kde cena = nakupni cena + 5%.
*/
public function getVIPPriceListId(): int
{
return 4;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Email;
use External\PompoBundle\Email\UserPOSRegistrationEmail;
class Preprocessor extends \KupShop\KupShopBundle\Util\Mail\Preprocessor
{
public function getUtmString(string $campaign): string
{
$utmString = parent::getUtmString($campaign);
if ($campaign === mb_strtolower(UserPOSRegistrationEmail::getType())) {
return $utmString.'#user-register-form';
}
return $utmString;
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Export;
use Query\Product;
use Query\QueryBuilder;
class OSSExport
{
public function export()
{
$qb = $this->getQueryBuilder();
header('Content-type: application/xls');
header('Content-Disposition: attachment; filename="products-oss-export.csv"');
$header = [
'Kód produktu',
'Název produktu',
'CN kód',
'DPH u produktu',
'DPH [CZ]',
'DPH [SK]',
];
$fp = fopen('php://output', 'w');
fputcsv($fp, $header);
foreach ($qb->execute() as $item) {
fputcsv($fp, $item, ';');
}
fclose($fp);
}
public function getQueryBuilder(): QueryBuilder
{
return sqlQueryBuilder()
->select('p.code, p.title, p.id_cn, v.vat')
->fromProducts()
->leftJoin('p', 'vats', 'v', 'v.id = p.vat')
->andWhere(Product::withVats(['CZ', 'SK']))
->groupBy('p.id');
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Export;
use External\PompoBundle\Util\Configuration;
use External\PompoBundle\Util\Ordering\OrderType;
use Query\QueryBuilder;
class OrdersExcelExport extends \KupShop\OrderingBundle\OrdersExcelExport
{
/** @required */
public Configuration $configuration;
protected function prepareFields()
{
$fields = parent::prepareFields();
$prependFields = [];
if ($this->configuration->isPompo()) {
$prependFields = [
'drsUserId' => [
'name' => 'Reg. zákazník',
'spec' => function (QueryBuilder $qb) {
$qb->leftJoin('o', 'drs_users', 'du', 'du.id_user = o.id_user');
return 'du.id_drs as drsUserId';
},
],
'sellerBranchId' => [
'name' => 'Cílová prodejna',
'spec' => function (QueryBuilder $qb) {
$qb->leftJoin('o', 'order_sellers', 'os', 'o.id = os.id_order')
->leftJoin('os', 'sellers', 'sell', 'sell.id = os.id_seller');
return 'JSON_VALUE(sell.data, "$.branchId") as sellerBranchId';
},
],
'expectedTod' => [
'name' => 'Datum závozu',
'spec' => function () {
return 'IF(JSON_VALUE(o.note_admin, "$.orderType")="'.OrderType::ORDER_TRANSPORT_RESERVATION.'", JSON_VALUE(o.note_admin, "$.delivery_data.deliveryDate"), "") as expectedTod';
},
],
'originOfGoods' => [
'name' => 'Odbavuje',
'spec' => function () {
return 'IF(JSON_VALUE(o.note_admin, "$.orderType")="'.OrderType::ORDER_RESERVATION.'", "Prodejna", "Centrala") as originOfGoods';
},
],
];
}
return array_merge($prependFields, $fields);
}
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Import;
use External\PompoBundle\DRS\Synchronizer\POSOrderSynchronizer;
use External\PompoBundle\DRS\Util\DRSApi;
use Query\Operator;
class PompoImport
{
private DRSApi $drsApi;
private POSOrderSynchronizer $posOrderSynchronizer;
private ?\Closure $writeLineCallback = null;
public function __construct(DRSApi $drsApi, POSOrderSynchronizer $posOrderSynchronizer)
{
$this->drsApi = $drsApi;
$this->posOrderSynchronizer = $posOrderSynchronizer;
}
public function setWriteLineCallback(callable $writeLineCallback): void
{
$this->writeLineCallback = $writeLineCallback;
}
public function importDRSCoupons(): void
{
$couponsMap = [
'Poukaz za body - NaturaMed' => 95,
'Poukaz za body - Sleva 500 Kč' => 69,
'Poukaz za body - TOBOGA' => 65,
'Poukaz za body - Dinopark Ostrava' => 61,
'Poukaz za body - Dinopark Praha' => 62,
'Poukaz za body - Fruitisimo' => 116,
'Poukaz za body - RAK' => 158,
'Poukaz za body - Aqualand Moravia' => 59,
'Poukaz za body - Chocotopia' => 160,
'Poukaz za body - POEX' => 161,
'Poukaz za body - Sleva 200 Kč' => 94,
'Voucher - Narozeniny' => 159,
'Poukaz za body - Sleva 250 Kč' => 68,
'Poukaz za body - Sleva 100 Kč' => 67,
'Poukaz za body - Sleva 50 Kč' => 93,
'Poukaz za body - IQLANDIA' => 126,
'Poukaz za body - Terezia' => 98,
'Voucher - Narozeniny - Kopie' => 159,
'Poukaz za body - Dinopark' => 60,
];
$count = 0;
foreach ($this->drsApi->getUsersCoupons() as $item) {
if (empty($item['code'])) {
continue;
}
$exists = sqlQueryBuilder()
->select('id')
->from('discounts_coupons')
->where(Operator::equals(['code' => $item['code']]))
->execute()->fetchOne();
// kupon uz existuje
if ($exists) {
// zaktualizuju platnost
sqlQueryBuilder()
->update('discounts_coupons')
->directValues(['used' => ($item['status'] == 2 || $item['status'] == 3) ? 'Y' : 'N'])
->where(Operator::equals(['id' => $exists]))
->execute();
continue;
}
$userId = sqlQueryBuilder()
->select('id_user')
->from('drs_users')
->where(Operator::equals(['id_drs' => $item['customerId']]))
->execute()->fetchOne();
// nemam usera
if (!$userId) {
continue;
}
// nemam ID generovanych kodu
if (!($discountId = ($couponsMap[$item['name']] ?? null))) {
continue;
}
// importuju kod
$couponId = sqlGetConnection()->transactional(function () use ($discountId, $userId, $item) {
sqlQueryBuilder()
->insert('discounts_coupons')
->directValues(
[
'id_discount' => $discountId,
'code' => $item['code'],
'date_from' => (new \DateTime($item['dateFrom']))->format('Y-m-d H:i:s'),
'date_to' => (new \DateTime($item['dateTo']))->format('Y-m-d H:i:s'),
'id_user' => $userId,
'used' => ($item['status'] == 2 || $item['status'] == 3) ? 'Y' : 'N',
]
)->execute();
return (int) sqlInsertId();
});
sqlQueryBuilder()
->insert('drs_user_coupons')
->directValues(
[
'id_drs' => $item['id'],
'id_coupon' => $couponId,
]
)->execute();
$count++;
}
foreach ($couponsMap as $_ => $discountId) {
$count = sqlQueryBuilder()
->select('COUNT(id)')
->from('discounts_coupons')
->where(Operator::equals(['id_discount' => $discountId]))
->execute()->fetchOne();
sqlQueryBuilder()
->update('discounts')
->directValues(
[
'uses_max' => $count,
]
)
->where(Operator::equals(['id' => $discountId]))
->execute();
}
$this->writeLine('Imported coupons: '.$count);
}
public function importPOSOrders(): void
{
$dbcfg = \Settings::getDefault();
$startTimestamp = $dbcfg->loadValue('drsPOSOrdersTimestampCustom');
if (!$startTimestamp) {
$startTimestamp = 38096494;
}
$timestampUpdater = function (int $timestamp) use ($dbcfg) {
$dbcfg->saveValue('drsPOSOrdersTimestampCustom', $timestamp, false);
};
$forceEnd = false;
do {
try {
$this->writeLine('Processing POS orders...');
$this->posOrderSynchronizer->processWithCustomTimestamp(
$startTimestamp,
$timestampUpdater
);
$forceEnd = true;
$this->writeLine('Done');
} catch (\Throwable $e) {
if (isDevelopment()) {
throw $e;
}
$this->writeLine('[ERROR] '.$e->getMessage());
sleep(30);
}
} while ($forceEnd === false);
}
public function importDRSOrders(): void
{
$dbcfg = \Settings::getDefault();
$forceEnd = false;
do {
$startDate = new \DateTime('2022-10-04');
// if ($timestamp = $dbcfg->loadValue('pompoOldOrdersTimestamp')) {
// $startDate = (new \DateTime())->setTimestamp($timestamp);
// }
try {
$this->writeLine('Processing orders...');
foreach ($this->drsApi->getOrders($startDate) as $order) {
if ($order instanceof \DateTime) {
$dbcfg->saveValue('pompoOldOrdersTimestamp', $order->getTimestamp(), false);
sleep(5);
continue;
}
$this->posOrderSynchronizer->processOrder($order);
}
$forceEnd = true;
} catch (\Exception $e) {
if (isDevelopment()) {
throw $e;
}
$this->writeLine('[ERROR] '.$e->getMessage());
sleep(30);
}
} while ($forceEnd === false);
$this->writeLine('Import done');
}
private function writeLine(string $message): void
{
call_user_func($this->writeLineCallback, $message);
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Incomaker;
use External\PompoBundle\Util\Configuration;
use KupShop\IncomakerBundle\Util\XMLElements\ContactXMLElement;
use Query\Operator;
class PompoContactXMLElement extends ContactXMLElement
{
private Configuration $configuration;
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
}
protected function getUsers(): array
{
$users = parent::getUsers();
// nafetchovat dodatecna data pro pompo
if ($this->configuration->isPompo()) {
$this->fetchUsersCards($users);
$this->fetchUsersChildren($users);
$this->fetchUserDRSId($users);
}
return $users;
}
protected function getCustomFields(array &$user)
{
$customFields = parent::getCustomFields($user);
foreach ($user['cards'] ?? [] as $i => $card) {
$customFields['card_code_'.$i] = ['key' => 'card', 'group' => 'card'.$i, 'value' => $card['code']];
$customFields['card_isActive_'.$i] = ['key' => 'isActive', 'group' => 'card'.$i, 'value' => $card['active']];
}
foreach ($user['children'] ?? [] as $i => $child) {
try {
$birthdate = (new \DateTime($child['date_birth']))->format('Y-m-d');
} catch (\Throwable $e) {
$birthdate = '';
}
$customFields['child_name_'.$i] = ['key' => 'name', 'group' => 'child'.$i, 'value' => $child['name']];
$customFields['child_birthday_'.$i] = ['key' => 'birthday', 'group' => 'child'.$i, 'value' => $birthdate];
$customFields['child_gender_'.$i] = ['key' => 'gender', 'group' => 'child'.$i, 'value' => $child['gender']];
}
$customFields['pompoId'] = ['key' => 'pompoId', 'value' => $user['pompoId'] ?? null];
unset($user['cards'], $user['children'], $user['pompoId']);
return $customFields;
}
/**
* Nafetchuje deti k uzivatelum.
*/
private function fetchUsersChildren(array &$users): void
{
$userIds = array_map(function ($x) { return $x['id']; }, $users);
$qb = sqlQueryBuilder()
->select('id_user, name, gender, date_birth')
->from('users_family')
->where(Operator::inIntArray($userIds, 'id_user'));
$children = [];
foreach ($qb->execute() as $item) {
$children[$item['id_user']][] = $item;
}
foreach ($users as &$user) {
$user['children'] = $children[$user['id']] ?? [];
}
}
/**
* Nafetchuje zakaznicke karty k uzivatelum.
*/
private function fetchUsersCards(array &$users): void
{
$userIds = array_map(function ($x) { return $x['id']; }, $users);
$qb = sqlQueryBuilder()
->select('id_user, code, active')
->from('user_cards')
->where(Operator::inIntArray($userIds, 'id_user'));
$cards = [];
foreach ($qb->execute() as $item) {
$cards[$item['id_user']][] = $item;
}
foreach ($users as &$user) {
$user['cards'] = $cards[$user['id']] ?? [];
}
}
/**
* Nafetchuje cislo zakaznika (id_drs).
*/
private function fetchUserDRSId(array &$users): void
{
$userIds = array_map(function ($x) { return $x['id']; }, $users);
$qb = sqlQueryBuilder()
->select('id_drs, id_user')
->from('drs_users')
->where(Operator::inIntArray($userIds, 'id_user'));
$pompoIds = [];
foreach ($qb->execute() as $item) {
$pompoIds[$item['id_user']] = $item['id_drs'];
}
foreach ($users as &$user) {
$user['pompoId'] = $pompoIds[$user['id']] ?? null;
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Incomaker;
use KupShop\IncomakerBundle\Util\XMLElements\ProductXMLElement;
use KupShop\KupShopBundle\Context\LanguageContext;
use Query\QueryBuilder;
class PompoProductXMLElement extends ProductXMLElement
{
private const CAMPAIGNS = ['LC', 'TP', 'QA', 'KA', 'EX', 'BC', 'CH'];
protected function createQB($specs = null): QueryBuilder
{
$this->productList->fetchStoresInStore();
return parent::createQB($specs);
}
protected function createXmlElement($product)
{
$product->inStore = $product->storesInStore[2]['in_store'] ?? 0; // 2 - Centrální sklad
parent::createXmlElement($product);
}
protected function getLanguagesAttributes(\Product $product, string $language): array
{
$attributes = parent::getLanguagesAttributes($product, $language);
// get campaigns with correctly translated names
$campaigns = $this->contextManager->activateContexts([LanguageContext::class => $language], function () use ($product) {
if (empty($product->campaign_codes)) {
return [];
}
$campaigns = implode(',', array_keys($product->campaign_codes));
productCampaign($campaigns, $codes);
$result = [];
foreach ($codes as $code => $item) {
if (in_array($code, self::CAMPAIGNS)) {
$result[$code] = $item['plural'];
}
}
return $result;
});
foreach ($campaigns as $key => $name) {
$attributes[] = ['attributes' => ['key' => $key], 'value' => $name];
}
return $attributes;
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Order;
use External\PompoBundle\Util\Ordering\OrderingUtil;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Query\Operator;
/**
* Common functions for order synchronization (DataGo, DRS).
*/
trait OrderSynchronizerTrait
{
protected function getOrderTypeSpec(?array $types): callable
{
$field = 'JSON_VALUE(COALESCE(o.note_admin, "{}"), "$.orderType")';
return $types === null ? Operator::equalsNullable([$field => $types]) : Operator::inStringArray($types, $field);
}
protected function getOrderNumber(\Order $order): string
{
if ($order->getFlags()['DSE'] ?? false) {
if ($expandoOrderId = ($order->getData('expando')['orderId'] ?? false)) {
return $expandoOrderId;
}
}
return $order->order_no;
}
protected function getOrderNote(\Order $order): string
{
$note = [(string) $order->note_user];
$discounts = $order->getData('discounts');
if (!empty($discounts['used_coupons'])) {
$note[] = 'Pouk: '.implode(', ', $discounts['used_coupons']);
}
return implode('; ', array_filter($note));
}
protected function isOrderItemTransportCharge(OrderItem $item): bool
{
if (($item->getNote()['item_type'] ?? false) === OrderItemInfo::TYPE_CHARGE) {
if (in_array($item->getNote()['id_charge'] ?? false, OrderingUtil::CHARGES_HANDLING_FEE)) {
return true;
}
}
return false;
}
protected function changeOrderStatus(\Order $order, int $status, ?string $emailType = null): void
{
if (!$this->isOrderStatusNext($order, $status)) {
return;
}
$forceSendMail = null;
// U Expando objednavek nechceme posilat maily
if ($order->getFlags()['DSE'] ?? false) {
$forceSendMail = false;
}
$order->changeStatus($status, null, $forceSendMail, null, $emailType);
}
/**
* Vrati true/false na zaklade toho, zda $status je vyssí stav, nez aktualni stav objednavky.
*
* Pouziva se ke kontrole, aby se neprovedo prepnuti stavu zpet (do nizsiho stavu).
*/
protected function isOrderStatusNext(\Order $order, int $status): bool
{
$statuses = array_keys(getOrderStatuses());
if (($orderStatusIndex = array_search($order->status, $statuses)) === false) {
return false;
}
if (($newStatusIndex = array_search($status, $statuses)) === false) {
return false;
}
if ($newStatusIndex > $orderStatusIndex) {
return true;
}
return false;
}
protected function isOrderInPerson(\Order $order): bool
{
if ($deliveryType = $order->getDeliveryType()) {
if ($delivery = $deliveryType->getDelivery()) {
if ($delivery instanceof \OdberNaProdejne || $delivery->isInPerson()) {
return true;
}
}
}
return false;
}
protected function setOrderPackageNumber(\Order $order, string $packageNumber): void
{
if (!empty($order->package_id)) {
return;
}
$order->package_id = $packageNumber;
sqlQueryBuilder()
->update('orders')
->directValues(
[
'package_id' => $packageNumber,
]
)
->where(Operator::equals(['id' => $order->id]))
->execute();
$order->logHistory(sprintf('[DataGo] Číslo balíku: %s', $packageNumber));
}
/** Returns split delivery and payment into single items */
protected function splitDeliveryItem(\Order $order, ?OrderItem $deliveryItem): array
{
if (!$deliveryItem || !($deliveryType = $this->getOrderDeliveryType($order))) {
return [];
}
$deliveryPrice = 0;
$paymentPrice = 0;
$deliveryItemPrice = $deliveryItem->getTotalPrice()->getPriceWithVat()->abs()->asFloat();
if ($deliveryItemPrice > 0 && $deliveryType->price_payment) {
$paymentPrice = $deliveryType->price_payment->getPriceWithVat()->asFloat();
$deliveryPrice = $deliveryItemPrice - $paymentPrice;
if ($deliveryPrice < 0) {
$deliveryPrice = 0;
}
}
$result = [];
if ($deliveryPrice > 0) {
$tmpDeliveryItem = [
'id' => 'D-'.$deliveryItem->getId(),
'price' => $deliveryPrice,
'vat' => $deliveryItem->getVat(),
'quantity' => $deliveryItem->getPieces(),
];
$result['delivery'] = $tmpDeliveryItem;
}
if ($paymentPrice > 0) {
$tmpPaymentType = [
'id' => 'P-'.$deliveryItem->getId(),
'price' => $paymentPrice,
'vat' => $deliveryItem->getVat(),
'quantity' => $deliveryItem->getPieces(),
];
$result['payment'] = $tmpPaymentType;
}
return $result;
}
protected function getOrderDeliveryType(\Order $order): ?\DeliveryType
{
if ($deliveryType = $order->getDeliveryType()) {
$deliveryType->accept(
new Price($order->getTotalPrice()->getPriceWithVat(), $order->getTotalPrice()->getCurrency(), 0),
false,
$order->getPurchaseState()
);
$deliveryType->getPrice();
return $deliveryType;
}
return null;
}
}

View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Order;
use External\PompoBundle\DataGo\Util\DataGoApi;
use External\PompoBundle\DRS\Synchronizer\OrderSynchronizer;
use External\PompoBundle\DRS\Util\DRSApi;
use External\PompoBundle\Util\PompoUtil;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use Query\Operator;
class PompoOrderUtil
{
public const SYNCHRONIZER_DATAGO = 'dataGo';
public const SYNCHRONIZER_DRS = 'drs';
private DRSApi $drsApi;
private DataGoApi $dataGoApi;
private PompoUtil $pompoUtil;
private OrderSynchronizer $drsOrderSynchronizer;
public function __construct(DRSApi $drsApi, DataGoApi $dataGoApi, PompoUtil $pompoUtil, OrderSynchronizer $drsOrderSynchronizer)
{
$this->drsApi = $drsApi;
$this->dataGoApi = $dataGoApi;
$this->pompoUtil = $pompoUtil;
$this->drsOrderSynchronizer = $drsOrderSynchronizer;
}
public function getOrderSynchronizerType(\Order $order): ?string
{
// pokud je to rezervace, tak jde objednavka pres DRS
if (($order->getFlags()['RZ'] ?? false) || ($order->getFlags()['RT'] ?? false)) {
return self::SYNCHRONIZER_DRS;
}
// jinak jde objednavka pres DataGo
return self::SYNCHRONIZER_DATAGO;
}
public function getOrderItemChargeCode(OrderItem $item): ?string
{
$chargeId = ($item->getNote()['charge']['id_charge'] ?? null) ?: ($item->getNote()['id_charge'] ?? null);
if (!$chargeId) {
return null;
}
$data = sqlQueryBuilder()
->select('data')
->from('charges')
->where(Operator::equals(['id' => $chargeId]))
->execute()->fetchOne();
$data = json_decode($data ?: '', true) ?: [];
if (!empty($data['pompo_sync_code'])) {
return $data['pompo_sync_code'];
}
return null;
}
public function getOrderItemDifference(\Order $order, array $orderItems = []): array
{
$currentItems = [];
$orderItems = empty($orderItems) ? $this->getOrderItemsCounts($order) : $orderItems;
try {
if ($order->getData('pompoCreateItemDiffFromLocalData')) {
// projdu polozky objednavky a vlozim si je do $currentItems
foreach ($order->fetchItems() as $item) {
$currentItems[$item['id']] = (float) $item['pieces'];
}
// pokud nejaka polozka v $currentItems chybi, ale v puvodnim stavu byla ($orderItems), tak ji doplnim
// do $currentItems s nulovym poctem kusu
foreach ($orderItems as $key => $quantity) {
if (!isset($currentItems[$key])) {
$currentItems[$key] = 0;
}
}
} elseif ($this->getOrderSynchronizerType($order) === self::SYNCHRONIZER_DRS) {
$currentItems = $this->drsApi->getOrderCurrentItems(
$order->order_no,
$this->drsOrderSynchronizer->getLoginBranch($order),
array_keys($orderItems)
);
} else {
$dataGoId = sqlQueryBuilder()
->select('id_datago')
->from('datago_orders')
->where(Operator::equals(['id_order' => $order->id]))
->execute()->fetchOne();
if ($dataGoId) {
[$user, $pass] = $this->pompoUtil->getDataGoCredentialsByOrder($order);
$currentItems = $this->dataGoApi->setCredentials($user, $pass)
->getOrderCurrentItems($dataGoId);
}
}
} catch (\Throwable $e) {
}
return $this->getOrderItemDifferenceResult($orderItems, $currentItems);
}
public function getOrderItemDifferenceResult(array $orderItems, array $dataGoItems): array
{
$hasDiff = false;
foreach ($orderItems as $id => $pieces) {
if (($dataGoItems[$id] ?? null) === null || ($dataGoItems[$id] == $pieces)) {
continue;
}
$hasDiff = true;
break;
}
$dataGoItems = empty($dataGoItems) ? [] : $dataGoItems;
return [
'hasDiff' => $hasDiff,
'items' => $dataGoItems,
];
}
public function getOrderChangedItems(\Order $order): array
{
$originalStates = $order->getData('orderOriginalStates') ?: [];
$originalState = reset($originalStates);
$orderItems = [];
if (!empty($originalState)) {
foreach ($originalState as $itemId => $originalItem) {
$orderItems[$itemId] = $originalItem['pieces'];
}
} else {
// pokud nemam original state, tak ho vyrobim
foreach ($this->getOrderItems($order) as $item) {
if ($item['id_product']) {
$originalState[$item->getId()] = $this->prepareSimpleOrderItem($item);
}
}
}
$result = $this->getOrderItemDifference($order, $orderItems);
$changedItems = [];
if ($result['hasDiff']) {
foreach ($originalState as $originalItemId => $originalItem) {
$newPieces = $result['items'][$originalItemId] ?? null;
if ($newPieces === null || $newPieces == $originalItem['pieces']) {
continue;
}
$changedItems[] = array_merge($originalItem, [
'oldPieces' => $originalItem['pieces'],
'newPieces' => $newPieces,
]);
}
}
return $changedItems;
}
public function updateOrderByDifference(\Order $order, array $diffData): void
{
$newItems = $diffData['items'] ?? [];
if (empty($newItems)) {
return;
}
$originalState = [];
$changeLog = [];
foreach ($this->getOrderItems($order) as $item) {
if (!$item['id_product']) {
continue;
}
$originalState[$item->getId()] = $this->prepareSimpleOrderItem($item);
$newPieces = $newItems[$item->getId()] ?? 0;
// nic se nezmenilo
if ($item->getPieces() == $newPieces) {
continue;
}
// aktualizuju polozky, ktera se zmenila
if ($newPieces > 0) {
$changeLog[] = sprintf('Změna počtu kusů u položky <strong>%s</strong> (%s): %s -> %s', $item->getDescr(), $item->getId(), $item->getPieces(), $newPieces);
$order->updateItem($item->getId(), $newPieces);
} else {
$changeLog[] = sprintf('Odebrána položka <strong>%s</strong> (%s): %s -> %s', $item->getDescr(), $item->getId(), $item->getPieces(), $newPieces);
$order->updateItem($item->getId(), 0);
}
}
if (!empty($changeLog)) {
// save original state
$originalStates = $order->getData('orderOriginalStates') ?: [];
$originalStates[] = $originalState;
$order->setData('orderOriginalStates', $originalStates);
if ($this->getOrderSynchronizerType($order) === self::SYNCHRONIZER_DRS) {
$logPrefix = '[DRS]';
} else {
$logPrefix = '[DataGo]';
}
// log info to order history
$changeLog = [-1 => '<strong>'.$logPrefix.' Změny položek v objednávce:</strong>'] + array_map(fn ($x) => ' - '.$x, $changeLog);
$order->logHistory(
implode('<br>', $changeLog)
);
}
}
public function getOrderItemsCounts(\Order $order): array
{
$orderItems = [];
foreach ($order->fetchItems() as $item) {
if ($item['id_product']) {
$orderItems[$item['id']] = $item['pieces'];
}
}
return $orderItems;
}
public function prepareSimpleOrderItem(OrderItem $item): array
{
return [
'id' => $item->getId(),
'productId' => $item->getProductId(),
'variationId' => $item->getVariationId(),
'pieces' => $item->getPieces(),
'price' => $item->getTotalPrice()->getPriceWithVat()->asFloat(),
'tax' => $item->getVat(),
'name' => $item->getDescr(),
];
}
/**
* @return OrderItem[]
*/
private function getOrderItems(\Order $order): array
{
return $order->getItems();
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Ordering;
/**
* Konstanty s typem objednávky.
*/
class OrderType
{
/**
* Čistě rezervace na prodejně. Nemá manipulační poplatek a objednávka jde pouze do DRSu - neodesílá se do DataGo.
*/
public const ORDER_RESERVATION = 'reservation';
/**
* Objednávka přes centrálu na prodejnu - má manipulační poplatek a objednávka jde jak do DRSu, tak do DataGo.
*/
public const ORDER_TRANSPORT_RESERVATION = 'transport_reservation';
/**
* Objednávka přes přepravce - jde do DataGo i do DRSu.
*/
public const ORDER_TRANSPORT = 'transport';
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Ordering;
/**
* Konstanty s typem dostupnosti.
*/
class ProductAvailability
{
/** Skladem - skladem komplet na prodejne nebo na hlavnim skladu */
public const IN_STORE = 4;
/** Skladem u dodavatele - produkt je skladem pouze u dodavatele */
public const IN_STORE_SUPPLIER = 3;
/** Nedostupné */
public const NOT_IN_STORE = 0;
/** Na prodejnu zavezeme - není skladem na prodejně, ale je skladem na hlavním skladu */
public const SELLER_IN_STORE_TRANSFER = 1;
/** Částečně skladem - neco je skladem na prodejne a neco je skladem na centralnim skladu, kombinace */
public const SELLER_IN_STORE_PARTIALLY = 2;
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use External\PompoBundle\Exception\PompoException;
use KupShop\KupShopBundle\Util\FileUtil;
use KupShop\KupShopBundle\Util\System\PathFinder;
class PompoContentUtil
{
public const CONTENT_TYPE_SECTION = 'section';
public const CONTENT_TYPE_PRODUCER = 'producer';
private PathFinder $pathFinder;
public function __construct(PathFinder $pathFinder)
{
$this->pathFinder = $pathFinder;
}
public function uploadContent(array $file, string $subfolder, string $type): bool
{
// zkontroluju, ze nahravaji ZIP soubor
if (!in_array($file['type'], $this->getZipMimeTypes())) {
throw new PompoException('Nahrávaný soubor musí být ZIP soubor');
}
$tmpPath = $this->pathFinder->dataPath($type.'/pompo_content/_tmp_'.$subfolder);
$savePath = $this->getContentPath($subfolder, $type);
// smazu aktualni content
if (file_exists($savePath)) {
FileUtil::deleteDir($savePath);
}
// vytvorim slozku
if (!file_exists($tmpPath)) {
mkdir($tmpPath, 0777, true);
}
$originalTmpPath = $tmpPath;
$zip = new \ZipArchive();
// rozbalim zip
if ($zip->open($file['tmp_name'])) {
// Rozbalim si ZIP do tmp slozky
$zip->extractTo($tmpPath);
$zip->close();
$files = array_filter(scandir($tmpPath), function ($x) { return !empty(trim($x, '.')); });
// Pokud je v ZIPu jeste slozka, ve ktere ten obsah je, tak to z ni vytahnu
if (count($files) === 1) {
if ($dir = reset($files)) {
$tmpPath .= '/'.$dir;
}
}
// Presunu soubory do slozky, kde je ocekavam
rename($tmpPath, $savePath);
}
// Smazu tmp slozku
FileUtil::deleteDir($originalTmpPath);
return true;
}
public function deleteCustomContent(string $subfolder, string $type): bool
{
$path = $this->getContentPath($subfolder, $type);
if (file_exists($path)) {
FileUtil::deleteDir($path);
}
return true;
}
public function downloadCustomContent(string $subfolder, string $type): void
{
$path = $this->getContentPath($subfolder, $type);
if (file_exists($path)) {
$zip = new \ZipArchive();
$tmpFile = tempnam($this->pathFinder->getTmpDir(), 'pompo_content_');
$zip->open($tmpFile, \ZipArchive::CREATE);
$dir = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS);
$files = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
// nahazet soubory do zipu
foreach ($files as $file) {
if ($file->isDir()) {
continue;
}
$zipName = ltrim($file->getPathname(), $path);
$zip->addFile($file->getPathname(), $zipName);
}
if ($result = $zip->close()) {
header('Content-disposition: attachment; filename='.$type.'_content_'.$subfolder.'.zip');
header('Content-type: application/zip');
readfile($tmpFile);
unlink($tmpFile);
return;
}
unlink($tmpFile);
throw new PompoException('Vlastní obsah se nepodařilo stáhnout.', '', [
'result' => $result,
'zipStatusString' => $zip->getStatusString(),
'type' => $type,
'folder' => $subfolder,
]);
}
throw new PompoException('Nebyl nalezen žádný vlastní obsah.');
}
private function getContentPath(string $subfolder, string $type): string
{
return $this->pathFinder->dataPath($type.'/pompo_content/'.$subfolder);
}
private function getZipMimeTypes(): array
{
return ['application/zip', 'application/octet-stream', 'application/x-zip-compressed', 'multipart/x-zip'];
}
}

View File

@@ -0,0 +1,443 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\ContentBundle\Util\Block;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\StringUtil;
use Query\Operator;
use WpjShop\GraphQL\Client;
use WpjShop\GraphQL\DataObjects\Product\CollectionItem;
use WpjShop\GraphQL\DataObjects\Product\LinkItem;
use WpjShop\GraphQL\DataObjects\Product\ParameterValue;
use WpjShop\GraphQL\DataObjects\Product\ProductParameter;
use WpjShop\GraphQL\DataObjects\Product\RelatedItem;
use WpjShop\GraphQL\Enums\ProductVisibility;
/**
* Slouží pro komunikaci mezi `prod/pompo` a `prod/nejhracka`. Z pompa se na nejhracku přenáší informace o produktu.
*/
class PompoGraphQLUpdater
{
private const GRAPHQL_URL = 'https://www.nejhracka.cz/admin/graphql';
private const GRAPHQL_ACCESS_TOKEN = '201ac0b08889d173f99cfc7230f3f327';
private ?Client $client = null;
private ProductList $productList;
private Block $block;
private ContextManager $contextManager;
private array $parametersCache = [];
private array $sectionsListCache = [];
private array $producersListCache = [];
public function __construct(ProductList $productList, Block $block, ContextManager $contextManager)
{
$this->productList = $productList;
$this->block = $block;
$this->contextManager = $contextManager;
}
public function updateEditableContent(int $id, array $data): bool
{
return $this->getClient()->editableContent->update($id, $data)['result'] ?? false;
}
public function updateProduct(int $id, array $data): array
{
return $this->getClient()->product
->addSelection(['descriptionPlus' => ['id']])
->update($id, $data) ?? [];
}
public function updateProductLinks(int $externalProductId, \Product $product): void
{
$links = [];
foreach ($product->links ?? [] as $link) {
$links[] = new LinkItem(
$link['title'],
$link['link'],
$link['type']
);
}
$this->client->product->updateLinks($externalProductId, $links);
}
public function updateProductRelated(int $externalProductId, \Product $product): void
{
$relatedProducts = [];
foreach ($product->products_related ?? [] as $item) {
if (empty($item->code)) {
continue;
}
if ($relatedExternalId = $this->getProductIdByCode($item->code)) {
$relatedProducts[] = new RelatedItem($relatedExternalId);
}
}
$this->client->product->updateRelated($externalProductId, $relatedProducts);
}
public function updateProductCollection(int $externalProductId, \Product $product): void
{
$collectionProducts = [];
foreach ($product->collections['own'] ?? [] as $item) {
if (empty($item->code)) {
continue;
}
if ($relatedExternalId = $this->getProductIdByCode($item->code)) {
$collectionProducts[] = new CollectionItem($relatedExternalId);
}
}
$this->client->product->updateCollections($externalProductId, $collectionProducts);
}
public function updateProductTranslations(int $externalProductId, int $externalEditableContentId, string $language, \Product $product): void
{
$translation = sqlQueryBuilder()
->select('*')
->from('products_translations')
->where(Operator::equals(['id_product' => $product->id, 'id_language' => $language]))
->execute()->fetchAssociative();
$productData = [];
if (!empty($translation['title'])) {
$productData['title'] = $translation['title'];
}
if (!empty($translation['short_descr'])) {
$productData['description'] = $translation['short_descr'];
}
if (!empty($translation['long_descr'])) {
$productData['longDescription'] = $translation['long_descr'];
}
if (!empty($translation['figure'])) {
$productData['visibility'] = $this->getProductVisibility($translation['figure']);
}
if (!empty($translation['meta_title']) || !empty($translation['meta_description']) || !empty($translation['meta_keywords'])) {
$seoData = [];
if (!empty($translation['meta_title'])) {
$seoData['title'] = $translation['meta_title'];
}
if (!empty($translation['meta_description'])) {
$seoData['description'] = $translation['meta_description'];
}
if (!empty($translation['meta_keywords'])) {
$seoData['keywords'] = $translation['meta_keywords'];
}
$productData['seo'] = $seoData;
}
if (!empty($productData)) {
$this->getClient()->product->translate($externalProductId, $language, $productData);
}
// prelozit popis+
if (!empty($product->id_block)) {
$defaultBlocks = $this->getBlocks((int) $product->id_block);
$translatedBlocks = $this->getBlocks((int) $product->id_block, $language);
if (json_encode($defaultBlocks) !== json_encode($translatedBlocks)) {
$this->getClient()->editableContent->translate(
$externalEditableContentId,
$language,
$this->createEditableContentFromBlocks($translatedBlocks)
);
}
}
}
public function updateProductParameters(int $externalProductId, \Product $product): void
{
$parameterUpdates = [];
foreach ($this->getParameters() as $externalParameter) {
$parameterUpdates[$externalParameter['id']] = [];
}
foreach ($product->param as $parameter) {
// pokud nenajdu parametr, tak ho vytvorim
if (!($externalParameterId = ($this->getParameterByName($parameter['name'])['id'] ?? false))) {
// nejdriv rozhodnu typ parametru
switch ($parameter['value_type']) {
case 'float':
$type = 'NUMBER';
break;
case 'char':
$type = 'TEXT';
break;
default:
$type = 'LIST';
}
// vytvorim parametr
$externalParameterId = $this->createParameter($parameter['name'],
$type,
empty($parameter['unit']) ? null : $parameter['unit']
)['id'];
}
$parameterUpdates[$externalParameterId] = array_map(fn ($x) => $x['value'], $parameter['values']);
}
// aktualizuju parametry na nejhracce
$productParameters = [];
foreach ($parameterUpdates as $externalParameterId => $values) {
$parameterType = $this->getParameterType($externalParameterId);
$productParameters[] = new ProductParameter(
$externalProductId,
$externalParameterId,
array_map(fn ($x) => new ParameterValue($x, $parameterType), $values)
);
}
$this->getClient()->product->updateParameterBulk($productParameters);
}
public function createParameter(string $name, string $type, ?string $unit = null): array
{
$result = $this->getClient()->parameter->create(
[
'name' => $name,
'type' => $type,
'unit' => $unit,
]
)['parameterCreate'];
// Pridam parametr do cache
return $this->parametersCache[] = $result;
}
public function getSections(bool $force = false): array
{
if (!$force && $this->sectionsListCache) {
return $this->sectionsListCache;
}
return $this->sectionsListCache = $this->getClient()->section->setSelection(
[
'id',
'name',
'visible',
'url',
'isVirtual',
'parent' => [
'id',
'name',
'parent' => [
'id',
'name',
'parent' => [
'id',
'name',
'parent' => [
'id',
'name',
'parent' => [
'id',
'name',
'parent' => [
'id',
'name',
],
],
],
],
],
],
]
)->list()['items'] ?? [];
}
public function getSectionIdByPath(array $path): ?int
{
static $sectionsByKey = [];
$createKey = function (array $path) {
return StringUtil::slugify(implode('/', $path));
};
if (empty($sectionsByKey)) {
$sections = $this->getSections();
foreach ($sections as $section) {
$remotePath = $this->getRemoteSectionPath($section);
$sectionsByKey[$createKey($remotePath)] = $section['id'];
}
}
$key = $createKey($path);
return $sectionsByKey[$key] ?? null;
}
public function getRemoteSectionPath(array $section): array
{
$path = [];
$currentSection = $section;
while ($currentSection) {
$path[$currentSection['id']] = $currentSection['name'];
$currentSection = $currentSection['parent'];
}
return array_reverse($path, true);
}
public function getProducers(): array
{
if (!empty($this->producersListCache)) {
return $this->producersListCache;
}
return $this->producersListCache = $this->client->producer->list()['items'] ?? [];
}
public function getParameters(bool $force = false): array
{
if (!$force && !empty($this->parametersCache)) {
return $this->parametersCache;
}
return $this->parametersCache = $this->getClient()->parameter->list()['items'] ?? [];
}
public function getParameterType(int $parameterId): string
{
static $parameterTypes;
if (!$parameterTypes) {
$parameterTypes = Mapping::mapKeys($this->getParameters(), fn ($k, $v) => [$v['id'], $v['type']]);
}
return $parameterTypes[$parameterId] ?? 'LIST';
}
public function getParameterByName(string $name): ?array
{
$nameLower = mb_strtolower($name);
foreach ($this->getParameters() as $parameter) {
if (mb_strtolower($parameter['name']) === $nameLower) {
return $parameter;
}
}
return null;
}
public function getProducerIdByName(string $name): ?int
{
$nameLower = mb_strtolower($name);
foreach ($this->getProducers() as $producer) {
if (mb_strtolower($producer['name']) === $nameLower) {
return $producer['id'];
}
}
return null;
}
public function getProductIdByCode(string $code): ?int
{
return $this->getClient()->product->getByCode($code)['id'] ?? null;
}
public function getProductVisibility(string $visibility): string
{
switch ($visibility) {
case 'Y':
return ProductVisibility::VISIBLE;
case 'O':
return ProductVisibility::CLOSED;
default:
return ProductVisibility::HIDDEN;
}
}
public function getBlocks(int $rootBlockId, ?string $language = null): ?array
{
$language = $language ?: Contexts::get(LanguageContext::class)->getDefaultId();
return $this->contextManager->activateContexts([LanguageContext::class => $language], function () use ($rootBlockId) {
return $this->block->getBlocks($rootBlockId);
});
}
public function getProduct(int $productId, ?string $language = null): ?\Product
{
$productList = clone $this->productList;
$productList->andSpec(Operator::equals(['p.id' => $productId]));
$productList->fetchParameters();
$productList->fetchSections();
$productList->fetchLinks();
$productList->fetchCollections();
$productList->fetchProductsRelated();
$productList->fetchProducers();
$language = $language ?: Contexts::get(LanguageContext::class)->getDefaultId();
$products = $this->contextManager->activateContexts([LanguageContext::class => $language], function () use ($productList) {
return $productList->getProducts();
});
if (empty($products[$productId])) {
return null;
}
return $products[$productId];
}
public function createEditableContentFromBlocks(array $blocks): array
{
$data = [
'areas' => [],
'overwrite' => true,
];
foreach ($blocks as $block) {
$data['areas'][] = [
'id' => null,
'name' => $block['name'],
'data' => $block['json_content'] ?: '[]',
];
}
return $data;
}
public function getClient(): Client
{
if (!$this->client) {
$this->client = new Client(
self::GRAPHQL_URL,
self::GRAPHQL_ACCESS_TOKEN
);
}
return $this->client;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use External\PompoBundle\DataGo\Exception\DataGoException;
use External\PompoBundle\Exception\PompoException;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use Psr\Log\LoggerInterface;
class PompoLogger
{
private ActivityLog $activityLog;
private SentryLogger $sentryLogger;
private LoggerInterface $logger;
public function __construct(ActivityLog $activityLog, SentryLogger $sentryLogger, LoggerInterface $logger)
{
$this->activityLog = $activityLog;
$this->sentryLogger = $sentryLogger;
$this->logger = $logger;
}
public function logException(\Throwable $e, string $message, array $data = []): void
{
if (isDevelopment()) {
throw $e;
}
// TODO: mozna casem odebrat, at logujeme jen do activity logu
if (!($e instanceof PompoException) && !($e instanceof \SoapFault)) {
$this->sentryLogger->captureException($e);
}
// PompoException na sobe muze mit i nejaky data na vic
if ($e instanceof PompoException) {
$prefix = $e instanceof DataGoException ? '[DataGo]' : '[DRS]';
$exceptionData = array_merge(['exceptionMessage' => $prefix.' '.$e->getMessage()], $e->getData());
if ($e->getDetailedMessage()) {
$exceptionData = array_merge(['detailedMessage' => $e->getDetailedMessage()], $exceptionData);
}
$data = array_merge($exceptionData, $data);
}
// Lock wait timeout a deadlock nechci logovat do ActivityLogu
if ($e instanceof LockWaitTimeoutException || $e instanceof DeadlockException) {
return;
}
$this->activityLog->addActivityLog(
ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
$message,
$data
);
}
public function log(string $message, array $data): void
{
if (isDevelopment()) {
return;
}
$this->logger->notice($message, $data);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use External\PompoBundle\Email\PompoPartnerCouponsEmail;
use Query\Operator;
class PompoNotifier
{
private const NOTIFY_LOW_PARTNER_COUPONS_COUNT = 5;
private PompoPartnerCouponsEmail $partnerCouponsEmail;
public function __construct(PompoPartnerCouponsEmail $partnerCouponsEmail)
{
$this->partnerCouponsEmail = $partnerCouponsEmail;
}
/**
* Upozornění v případě, že budou docházet partnerské kódy.
*/
public function notifyLowPartnerCoupons(): void
{
if (!($notifyEmail = $this->getNotifyEmail())) {
return;
}
$qb = sqlQueryBuilder()
->select('bpe.id, bpe.name, COUNT(dc.id) as count, MIN(dc.date_to) as min_date')
->from('bonus_points_exchange', 'bpe')
->leftJoin('bpe', 'discounts_coupons', 'dc', 'dc.id_discount = bpe.id_discount AND dc.used = \'N\' AND dc.id_user IS NULL AND (dc.date_to >= NOW() OR dc.date_to IS NULL)')
->where(Operator::equals(['bpe.partner' => 'Y']))
->groupBy('bpe.id');
$lowPartnerCoupons = [];
foreach ($qb->execute() as $item) {
if ($item['count'] <= self::NOTIFY_LOW_PARTNER_COUPONS_COUNT) {
$lowPartnerCoupons[] = $item;
}
}
if (!empty($lowPartnerCoupons)) {
$this->partnerCouponsEmail->setCoupons($lowPartnerCoupons);
$email = $this->partnerCouponsEmail->getEmail();
$email['to'] = $notifyEmail;
$this->partnerCouponsEmail->sendEmail($email);
}
}
private function getNotifyEmail(): ?string
{
$dbcfg = \Settings::getDefault();
$email = $dbcfg->loadValue('pompoSettings')['notify_email'] ?? null;
if (empty($email)) {
return null;
}
return $email;
}
}

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use External\PompoBundle\Admin\Tabs\PompoSettingsTab;
use External\PompoBundle\DataGo\Synchronizer\ProductSynchronizer;
use External\PompoBundle\DataGo\Util\DataGoLocator;
use External\PompoBundle\DRS\Synchronizer\SynchronizerInterface;
use External\PompoBundle\DRS\Util\DRSApi;
use External\PompoBundle\DRS\Util\DRSLocator;
use KupShop\DropshipBundle\Transfer\BaseLinkerTransfer;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\SellerBundle\Utils\SellerUtil;
use Query\Operator;
class PompoUtil
{
public function __construct(
private DataGoLocator $dataGoLocator,
private DRSLocator $drsLocator,
private DRSApi $drsApi,
private OrderInfo $orderInfo,
private ?SellerUtil $sellerUtil,
private PompoLogger $pompoLogger,
) {
}
/**
* Vrací jméno a heslo pro zápis objednávek do DataGo, protože každá prodejna nebo jazyková mutace má jiné údaje.
*/
public function getDataGoCredentialsByOrder(\Order $order): array
{
$dbcfg = \Settings::getDefault();
// pokud je to Expando objednávka
if ($order->getFlags()['DSE'] ?? false) {
if ($expandoCredentials = $this->getDataGoExpandoCredentials($order)) {
return $expandoCredentials;
}
}
if ($dropshipmentCredentials = $this->getDataGoDropshipmentCredentials($order)) {
return $dropshipmentCredentials;
}
// v pripade, ze mame prodejce
if ($this->sellerUtil) {
// pokud je objednavka s vyzvednutim na prodejne, tak vezmu ID prodejce
if ($sellerId = ($order->getData('delivery_data')['seller_id'] ?? false)) {
// pokud sellera nenajdu, tak zkusim prenacist sellery, protoze synchronizace bezu v jednom cykly, takze se ta member cache
// muze drzet treba i mesic, takze kdyz pridaji noveho sellera, tak v te cache chybi... takze zkusim znovu prenacist sellery
if (!$this->sellerUtil->getSeller((int) $sellerId)) {
$this->sellerUtil->getSellers(true);
}
if ($seller = $this->sellerUtil->getSeller((int) $sellerId)) {
// z prodejce si z custom dat vytahnu udaje pro zapis objednavky
$dataGo = $seller['data']['datago'] ?? [];
if (!empty($dataGo['user']) && !empty($dataGo['pass'])) {
return [$dataGo['user'], $dataGo['pass']];
}
}
}
}
// defaultni udaje pro zapis objednavky do DataGo podle jazyka
if ($credentials = ($dbcfg->datago['default'][$order->getLanguage()] ?? null)) {
return [$credentials['user'], $credentials['password']];
}
// Pokud nemam v adminu vyplnene default udaje, tak pouziju default udaje na tvrdo
if ($order->getLanguage() === 'sk') {
return ['eshopsk', 'kspohse'];
}
return ['PompoShop', 'pohSopmoP'];
}
public function getDataGoDropshipmentCredentials(\Order $order): ?array
{
if (!($dropshipmentInfo = $this->orderInfo->getOrderDropshipmentInfo($order->id))) {
return null;
}
$dropshipment = $dropshipmentInfo['dropshipment'];
if ($dropshipment['type'] !== BaseLinkerTransfer::getType()) {
return null;
}
if (!($marketplace = $dropshipmentInfo['external']['data']['marketplace'] ?? null)) {
return null;
}
$marketplacesCredentials = [];
foreach ($dropshipment['data']['pompo']['datago'] ?? [] as $item) {
$marketplacesCredentials[$item['marketplace']][$item['country'] ?? null] = $item;
}
$credentials = $marketplacesCredentials[$marketplace][$order->delivery_country] ?? $marketplacesCredentials[$marketplace][null] ?? null;
if (!$credentials) {
return null;
}
return [$credentials['user'], $credentials['password']];
}
/** Vrací jméno a heslo pro zápis objednávky do DataGo pro Expando objednávku. Jméno a heslo podle marketplace, případně ještě podle země. */
public function getDataGoExpandoCredentials(\Order $order): ?array
{
// nactu si marketplace dane objednavky
$marketplace = $order->getData('expando')['marketplace'] ?? null;
if (!$marketplace) {
return null;
}
$key = StringUtil::slugify($marketplace);
$dbcfg = \Settings::getDefault();
// vychozi udaje pro dany marketplace
$marketplaceDefaultCredentials = $dbcfg->datago['default'][$key] ?? null;
if (empty($marketplaceDefaultCredentials['user']) || empty($marketplaceDefaultCredentials['password'])) {
$marketplaceDefaultCredentials = $dbcfg->datago['default'][PompoSettingsTab::MARKETPLACE_DEFAULT_KEY] ?? null;
}
// udaje podle marketplacu a podle zeme
$credentials = $dbcfg->datago['default'][$key][$order->delivery_country] ?? null;
if (empty($credentials['user']) || empty($credentials['password'])) {
$credentials = $marketplaceDefaultCredentials;
}
if (empty($credentials['user']) || empty($credentials['password'])) {
return null;
}
return [$credentials['user'], $credentials['password']];
}
/**
* Vrací výchozí jméno a heslo pro zápis objednávek podle jazykové mutace.
*/
public function getDataGoDefaultCredentials(?string $language = null): array
{
$language = $language ?: Contexts::get(LanguageContext::class)->getActiveId();
$dbcfg = \Settings::getDefault();
if ($credentials = ($dbcfg->datago['default'][$language] ?? null)) {
return [$credentials['user'], $credentials['password']];
}
if ($language === 'sk') {
return ['eshopsk', 'kspohse'];
}
return ['PompoShop', 'pohSopmoP'];
}
/**
* Přepočítání skladu. Přenese sklad ze stores_items na produkt, aby seděla zásoba podle stores_items.
*/
public function recalculateStores(): void
{
$getQuantitySubQuery = function (bool $variations = false) {
$alias = 'p';
if ($variations) {
$alias = 'pv';
}
sqlQueryBuilder()
->update('stores_items')
->set('quantity', 0)
->where('quantity < 0')
->execute();
$qb = sqlQueryBuilder()
->select('COALESCE(SUM(GREATEST(si.quantity, 0)), 0)')
->from('stores_items', 'si')
->where('si.id_product = '.$alias.'.id');
if ($variations) {
$qb->andWhere('si.id_variation = pv.id');
} else {
$qb->andWhere('si.id_variation IS NULL');
}
if ($ignoredStores = $this->getIgnoredStores()) {
$qb->andWhere(
Operator::not(Operator::inIntArray($ignoredStores, 'si.id_store'))
);
}
return $qb;
};
$productsSubQuery = $getQuantitySubQuery();
sqlQuery('UPDATE products p
SET in_store = ('.$productsSubQuery->getSQL().')
WHERE in_store != ('.$productsSubQuery->getSQL().');', $productsSubQuery->getParameters(), $productsSubQuery->getParameterTypes());
$variationsSubQuery = $getQuantitySubQuery(true);
sqlQuery(
'UPDATE products_variations pv
SET pv.in_store = ('.$variationsSubQuery->getSQL().')
WHERE pv.in_store != ('.$variationsSubQuery->getSQL().');', $variationsSubQuery->getParameters(), $variationsSubQuery->getParameterTypes());
\Variations::recalcInStore();
}
/**
* Vrací sklady, které se mají ignorovat. Nemá se přenášet jejich skladovost na produkt.
*/
public function getIgnoredStores(): array
{
return array_map(function ($x) {
return $x['id'];
},
sqlQueryBuilder()
->select('id')
->from('stores')
->where(Operator::equals(['disabled' => 1]))
->execute()->fetchAllAssociative()
);
}
/**
* Spustí synchronizaci produktů s DataGo.
*/
public function synchronizeDataGoProducts(array $processTypes): void
{
foreach ($processTypes as $processType) {
try {
$synchronizer = $this->dataGoLocator->get(
ProductSynchronizer::getType()
);
$synchronizer->setMode(\External\PompoBundle\DataGo\Synchronizer\SynchronizerInterface::MODE_CSV);
$synchronizer->setProcessType($processType);
$synchronizer->process();
} catch (\Throwable $e) {
if (isDevelopment()) {
throw $e;
}
$this->pompoLogger->logException($e, '[DataGo] Během synchronizace produktů se vyskytla chyba', [
'processType' => $processType,
'message' => $e->getMessage(),
]);
}
}
}
/**
* Spustí synchronizaci zadaných typů s DataGo.
*/
public function synchronizeDataGo(array $types, int $mode = \External\PompoBundle\DataGo\Synchronizer\SynchronizerInterface::MODE_NORMAL): void
{
foreach ($types as $type) {
try {
$synchronizer = $this->dataGoLocator->get($type);
$synchronizer->setMode($mode);
$synchronizer->process();
} catch (\Throwable $e) {
if (isDevelopment()) {
throw $e;
}
$this->pompoLogger->logException($e, '[DataGo] Během synchronizace se vyskytla chyba', [
'type' => $type,
'message' => $e->getMessage(),
]);
}
}
}
/**
* Spustí synchronizaci zadaných typů s DRSem.
*/
public function synchronizeDRS(array $types, int $mode = SynchronizerInterface::MODE_NORMAL): void
{
foreach ($types as $type) {
try {
$synchronizer = $this->drsLocator->get($type);
$synchronizer->setMode($mode);
$synchronizer->process();
} catch (\Throwable $e) {
if (isDevelopment()) {
throw $e;
}
$this->pompoLogger->logException($e, '[DRS] Během synchronizace se vyskytla chyba', [
'type' => $type,
'message' => $e->getMessage(),
]);
}
}
$this->drsApi->closeConnection();
}
}

View File

@@ -0,0 +1,756 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util;
use KupShop\CatalogBundle\ProductList\MultiFetch;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\DateUtil;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
use KupShop\LabelsBundle\Util\LabelUtil;
use KupShop\SellerBundle\Context\SellerContext;
use Query\Operator;
use Query\Product;
class ProductUtil
{
private const int MAIN_STORE_ID = 2;
private const int SUPPLIER_ID = 3;
public function __construct(
private Configuration $configuration,
private ProductList $productList,
private LabelUtil $labelUtil,
) {
}
/**
* Multifetch pro nafetchovani dalsich cen, ktere potrebujeme zobrazovat na katalogu / detailu produktu.
*
* Nafetchuje DMOC cenu, VIP cenu a "Pompo klub setri" cenu.
* Zaroven to prida dmocDiscount, coz je sleva vypocitana z DMOC ceny.
*/
public function fetchAdditionalPrices(ProductCollection $products): void
{
if ($products->count() === 0) {
return;
}
if (isset($products->_pompoVipPricesFetched)) {
return;
}
$products->fetchPricelists();
$this->fetchDefaultPriceForDiscount($products);
$currencyContext = Contexts::get(CurrencyContext::class);
// pokud existuje cenik s DMOC, tak ho nactu
if ($dmocPriceListId = ($this->configuration->getDMOCPriceLists()[$currencyContext->getActiveId()] ?? false)) {
foreach ($products as $product) {
$product->dmocPrice = $product->pricelists[$dmocPriceListId]['productPrice'] ?? null;
$product->dmocDiscount = \DecimalConstants::zero();
if ($product->dmocPrice) {
$product->dmocPrice = PriceCalculator::convert($product->dmocPrice, $product->getProductPrice()->getCurrency());
$productPriceWithVat = $product->getProductPrice()->getPriceWithVat();
$dmocPriceWithVat = $product->dmocPrice->getPriceWithVat();
if ($dmocPriceWithVat->isPositive()) {
$product->dmocDiscount = \DecimalConstants::one()->sub($productPriceWithVat->div($dmocPriceWithVat))->mul(\DecimalConstants::hundred());
} else {
$product->dmocDiscount = \DecimalConstants::zero();
}
}
}
}
// pokud existuje cenik pro prihlaseneho uzivatele, tak nactu VIP cenu
if ($vipPriceListId = ($this->configuration->getUsersPriceLists()[$currencyContext->getActiveId()] ?? false)) {
$userIsActive = Contexts::get(UserContext::class)->isActive();
foreach ($products as $product) {
$product->userVipPrice = $product->pricelists[$vipPriceListId]['productPrice'] ?? null;
$product->vipOriginalPrice = null;
// pokud jsem prihlasenej a existuje VIP cena, tak chci nacist "Pompo setri" cenu
if ($userIsActive && $product->userVipPrice) {
$userVipPrice = $product->userVipPrice;
// puvodni cena produktu (neprihlaseny uzivatel)
// pokud mam puvodni cenu brat z ceniku
if ($originalPriceListId = ($this->configuration->getPriceLists()[$currencyContext->getActiveId()] ?? false)) {
$originalPrice = $product->pricelists[$originalPriceListId]['productPrice'] ?? $product->priceOriginal->getObject();
$defaultPriceForDiscount = $originalPrice->getPriceWithoutDiscount();
if (!empty($product->pricelists[$originalPriceListId]['price_for_discount'])) {
$defaultPriceForDiscount = new ProductPrice(
toDecimal($product->pricelists[$originalPriceListId]['price_for_discount']),
Contexts::get(CurrencyContext::class)->getOrDefault($product->pricelists[$originalPriceListId]['currency']),
$product->vat
);
}
} else {
$originalPrice = $product->priceOriginal->getObject();
$defaultPriceForDiscount = $product->defaultPriceForDiscount ?? $originalPrice->getPriceWithoutDiscount();
}
// prevod cen na stejnou menu
PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false);
// pokud je VIP cena i original cena stejna
$vipSavingsEnabled = true;
if ($userVipPrice->getPriceWithVat()->equals($originalPrice->getPriceWithVat())) {
$originalPrice = $defaultPriceForDiscount;
$vipSavingsEnabled = false;
}
// prevod cen na stejnou menu
PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false);
$product->priceForDiscount = $originalPrice;
$product->originalPrice = $originalPrice;
$product->vipOriginalPrice = $originalPrice;
// spocitam rozdil - tim zjistim kolik mi setri Pompo klub
$diff = $originalPrice->getPriceWithVat()->sub($userVipPrice->getPriceWithVat());
$product->vipSavingsPrice = null;
// pokud je rozdil vetsi jak 0, tak ho ulozim at ho muzu zobrazit
if ($vipSavingsEnabled && $diff->isPositive()) {
$product->vipSavingsPrice = new Price($diff, $userVipPrice->getCurrency(), 0);
}
}
}
}
$products->_pompoVipPricesFetched = true;
}
public function fetchDefaultPriceForDiscount(ProductCollection $products): void
{
if ($products->count() === 0) {
return;
}
$qb = sqlQueryBuilder()
->select('id, COALESCE(price_for_discount, price) as price_for_discount, vat')
->from('products')
->where(Operator::inIntArray($products->getProductIds(), 'id'));
$defaultCurrency = Contexts::get(CurrencyContext::class)->getDefault();
foreach ($qb->execute() as $item) {
if (isset($products[$item['id']]) && !empty($item['price_for_discount'])) {
$price_for_discount = toDecimal($item['price_for_discount']);
$products[$item['id']]->defaultPriceForDiscount = new ProductPrice($price_for_discount, $defaultCurrency, getVat($item['vat']));
}
}
}
/**
* Multifetch pro nafetchovani informaci okolo skladu / prodejen, ktere se pouzivaji v katalogu, v kosiku atd...
*
* Nafetchuje skladovosti rozdelene podle skladu - jde o centralni sklad, sklad aktualne vybrane prodejny a soucet kusu na ostatnich prodejnach.
*/
public function fetchStoreInfo(ProductCollection $products): void
{
if (!findModule(\Modules::SELLERS) || $products->count() === 0) {
return;
}
static $storeIdBySeller = [];
$sellerContext = Contexts::get(SellerContext::class);
// nacist ID aktivniho skladu podle aktivni prodejny
if (!($storeIdBySeller[$sellerContext->getActiveId()] ?? false)) {
$storeId = sqlQueryBuilder()
->select('id_store')
->from('sellers')
->where(Operator::equals(['id' => $sellerContext->getActiveId()]))
->execute()->fetchOne();
if ($storeId) {
$storeIdBySeller[$sellerContext->getActiveId()] = (int) $storeId;
}
}
$activeStoreId = $storeIdBySeller[$sellerContext->getActiveId()] ?? null;
// nemusim to fetchovat znova, protoze uz to je fetchnuty
if ($products->_pompoStoresFetched[$activeStoreId] ?? false) {
return;
}
// multifetchu informace o skaldech
$products->fetchStoresInStore();
$products->fetchProductOfSuppliersInStore();
$sellersByStoreId = $this->getSellersByStoreId();
// k produktum doplnim potrebne informace
foreach ($products as $product) {
// skladovost na centralnim skladu
$product->inStoreMain = (int) ($product->storesInStore[$this->configuration->getMainStoreId()]['in_store'] ?? 0);
// skladovost u dodavatele
$product->inStoreSupplier = (int) ($product->in_store_suppliers ?? 0);
// skladovost na moji prodejne
$product->inStoreSeller = (int) ($product->storesInStore[$activeStoreId]['in_store'] ?? 0);
// skladovost na ostatnich prodejnach
$inStoreSellerOther = 0;
foreach ($product->storesInStore ?? [] as $storeId => $item) {
// v $sellersByStoreId mam prodejce indexovany podle ID skladu
// zaroven tam jsou odfiltrovany prodejny podle aktivniho jazyku - pro cs jen CZ prodejny a pro sk jen SK prodejny
// takze na CZ shopu nebudeme zobrazovat skladovost SK prodejen, protoze SK prodejna stejne na CZ shopu nejde vybrat
if (!($sellersByStoreId[$storeId] ?? false)) {
continue;
}
$inStoreSellerOther += max(0, $item['in_store']);
}
$product->inStoreSellerOther = (int) $inStoreSellerOther;
}
if (!isset($products->_pompoStoresFetched)) {
$products->_pompoStoresFetched = [];
}
$products->_pompoStoresFetched[$activeStoreId] = true;
}
/**
* Vrací, zda je daný produkt skladem u marketplace dodavatele.
*/
public function isInStoreMarketplace(\Product $product, float $quantity): bool
{
$marketplaceSuppliers = $this->configuration->getMarketplaceSuppliers();
$suppliers = $product->getSuppliers();
foreach ($marketplaceSuppliers as $marketplaceSupplierId => $_) {
if (($suppliers[0][$marketplaceSupplierId] ?? 0) > $quantity) {
return true;
}
}
return false;
}
/**
* Vrátí datum dopravy navýšený a potřebný počet dnů.
*
* @deprecated `External\PompoBundle\Overrides\Order\DeliveryInfo::calculateCollectionDate` is used instead
*/
public function getDeliveryDateIncremented(\DateTime $deliveryDate, array $items, ?\Delivery $delivery = null): \DateTime
{
return $deliveryDate;
}
public function getDeliveryExpeditionDate(\Delivery $delivery): \DateTime
{
// Kontroluju kvuli tomu, zda se stihne odeslat jeste dnes nebo ne
$expeditionDate = new \DateTime();
$timeHours = '13:00:00';
if (!empty($delivery->time_hours)) {
$timeHours = $delivery->time_hours;
}
try {
$hourParts = explode(':', $timeHours);
$maxHoursToday = (new \DateTime())
->setTime((int) ($hourParts[0] ?? 0), (int) ($hourParts[1] ?? 0), (int) ($hourParts[2] ?? 0));
// Pokud se nestihne odeslat jeste dnes, tak navysim datum jeste o jeden den
if ((new \DateTime()) > $maxHoursToday) {
$expeditionDate = DateUtil::calcWorkingDays(1, $expeditionDate);
}
} catch (\Throwable $e) {
}
return $expeditionDate;
}
/**
* Vrací date increment - počet dní o kolik se má navýšit datum doručení.
*
* Date increment se urcuje podle dodavatele, zavrene expedice z centraly nebo podle specialne nastaveneho incrementu pro dany den.
*/
public function getDateIncrement(array $items, ?array $orderedQuantities = [], ?\DateTime $expeditionDate = null): int
{
$increment = 0;
if (empty($items)) {
return $increment;
}
$filterItems = [];
foreach ($items as $key => $_) {
$parts = explode('/', (string) $key);
$filterItems[$parts[0]] = $filterItems[$parts[0]] ?? null;
if ($parts[1] ?? null) {
$filterItems[$parts[0]][] = $parts[1];
}
}
// nactu si produkty
$products = $this->getProducts($filterItems);
// fetchnu si k produktum dodatecne info
$this->fetchProductsSupplierDeliveryDateIncrement($products);
$this->fetchStoreInfo($products);
// projdu produkty
foreach ($products as $key => $product) {
// pokud je to nejhracka, tak nemame prodejny a zajima nas inStore field na produktu, ale pokud
// jsme na pompu, tak nas zajima inStoreMain, kde je skladovost na hlavnim skladu bez prodejen
$inStoreMain = $this->configuration->isNejhracka() ? $product->inStore : ($product->inStoreMain ?? 0);
$tmpIncrement = 0;
// pocet kusu skladem, ktere objednavam
$requiredQuantity = $items[$key] ?? 0;
// pocet kusu, ktere jsou objednane v uz vytvorene objednavce
$orderedQuantity = $orderedQuantities[$key] ?? 0;
// pocet kusu skladem na hlavnim skladu
$inStore = $inStoreMain + min($orderedQuantity, $inStoreMain);
// pocet kusu skladem u dodavatele
$inStoreSuppliers = $product->in_store_suppliers;
// navysit increment podle skladovosti - pokud neni skladem na hlavnim skladu, ale je skladem u dodavatele, tak dorucuji pozdeji
if ($inStore < $requiredQuantity && $inStoreSuppliers > 0) {
$tmpIncrement = (int) ($product->supplierDeliveryDateIncrement ?? 0);
if ($supplierIncrement = $this->getSupplierOrderIncrement($product->_pompoSupplierId ?? self::SUPPLIER_ID)) {
$tmpIncrement += $supplierIncrement;
}
}
$increment = max($increment, $tmpIncrement);
}
$expeditionDate ??= new \DateTime();
// pokud je napr. expedice z centraly v dany den uzavrena, tak musim doruceni navysit
if ($mainStoreIncrement = $this->getMainStoreDeliveryDateIncrement($expeditionDate)) {
$increment += $mainStoreIncrement;
}
// pokud je v dany den nastaven nejaky dodatecny increment (Nastaveni e-shopu, Pompo a "Navýšení data doručení"
if ($dayIncrement = $this->getDeliveryIncrementByDay($expeditionDate)) {
$increment += $dayIncrement;
}
return $increment;
}
/**
* Vrací datumy, kdy je hlavní sklad uzavřen a tímpádem je uzavčena i expedice z něj.
*/
public function getMainStoreClosedDates(): array
{
$dbcfg = \Settings::getDefault();
$from = $dbcfg->datago['mainStoreClosed']['from'] ?? null;
$to = $dbcfg->datago['mainStoreClosed']['to'] ?? null;
if (!empty($from) && !empty($to)) {
try {
$dateFrom = (new \DateTime($from))->setTime(0, 0);
$dateTo = (new \DateTime($to))->setTime(23, 59, 59);
return [$dateFrom, $dateTo];
} catch (\Throwable $e) {
}
}
return [null, null];
}
/**
* Vrátí počet dní, o které je potřeba navýšit datum doručení pokud je expedice z centrály uzavřena.
* Např. kvůli inventuře, svátku, nebo Vánocům.
*/
public function getMainStoreDeliveryDateIncrement(\DateTime $expeditionDate): int
{
$increment = 0;
[$from, $to] = $this->getMainStoreClosedDates();
if (!empty($from) && !empty($to)) {
try {
// Pokud je centralni sklad z nejakyho duvodu uzavren
if ($expeditionDate >= $from && $expeditionDate <= $to) {
// rozdil mezi datumama
$diff = (new \DateTime())->diff($to);
// Ensure the increment is at least 1 day
$increment = max(1, abs($diff->days));
}
} catch (\Throwable $e) {
}
}
return $increment;
}
public function getDeliveryIncrementByDay(\DateTime $expeditionDate): int
{
$dbcfg = \Settings::getDefault();
if ($increment = $dbcfg->pompoSettings['dayDeliveryIncrement'][$expeditionDate->format('N')] ?? 0) {
return (int) $increment;
}
return 0;
}
/**
* Odecte od incrementu nepracovni dny, aby kdyz se zavola DateUtil::calcWorkingDays nebylo datum o ty nepracovni dny posunuty.
*
* TODO: Zpusobilo to problemy s datumem doruceni pred svatkama (4.7.2023). Upravovalo se to pred Vanocema, takze tam to taky delalo nejakej problem. Nasimulovat, projit a vyresit!
*/
private function getDateIncrementWithoutWorkdays(int $increment): int
{
if ($increment <= 0) {
return $increment;
}
foreach (range(1, $increment) as $i) {
$date = (new \DateTime())->add(new \DateInterval('P'.$i.'D'));
if (!DateUtil::isWorkday($date)) {
$increment--;
}
}
return $increment;
}
/**
* Vraci pocet dnu do dalsiho objednavaciho dne od dodavatele.
*/
private function getSupplierOrderIncrement(int $supplierId = self::SUPPLIER_ID): int
{
static $supplierIncrement = [];
if (!($supplierIncrement[$supplierId] ?? false)) {
$supplier = sqlQueryBuilder()
->select('*')
->from('suppliers')
->where(Operator::equals(['id' => $supplierId]))
->execute()->fetchAssociative();
$supplier['data'] = json_decode($supplier['data'] ?? '', true) ?? [];
$today = new \DateTime();
$orderHours = array_filter($supplier['data']['order_hours'] ?? []);
$increment = null;
// zkontrolovat, zda se bude dneska jeste objednavat od dodavatele
if ($todayOrderHour = $orderHours[$today->format('N')] ?? null) {
[$hour, $minute] = explode(':', $todayOrderHour);
$todayOrderDate = (new \DateTime())->setTime((int) $hour, (int) $minute);
// stiham objednat jeste dneska
if ($today < $todayOrderDate) {
$increment = 0;
}
}
if ($increment === null) {
// najit nejblizsi den, kdy se delaji objednavky dodavatelovi
$increment = $this->recursivelyGetClosestDeliveryDateIncrement($supplier['data']['order_hours'] ?? []);
}
$supplierIncrement[$supplierId] = $increment;
}
return $supplierIncrement[$supplierId];
}
/**
* Rekurzivne prochazi objednaci casy nastavene u dodavatele, aby se nasel nejblizsi den, kdy se od dodavatele bude objednavat.
*/
private function recursivelyGetClosestDeliveryDateIncrement(array $orderHours, int $increment = 0, bool $deep = false): int
{
$today = new \DateTime();
$found = false;
foreach ($orderHours as $day => $hour) {
if (!$deep && $day <= $today->format('N')) {
continue;
}
// if is weekday and hour is empty, skip
// weekday should not be added to increment, because we are adding only working days
if (in_array($day, [6, 7]) && empty($hour)) {
continue;
}
$increment++;
if (!empty($hour)) {
$found = true;
break;
}
}
if (!$found) {
if ($deep) {
return 0;
}
$increment = $this->recursivelyGetClosestDeliveryDateIncrement($orderHours, $increment, true);
}
return $increment;
}
/**
* Nafetchuje k produktum increment podle zaznamu v products of suppliers - kazdy produkt muze byt totiz delivery increment jeste povyseny.
*/
public function fetchProductsSupplierDeliveryDateIncrement(ProductCollection $products): ProductCollection
{
if ($products->count() === 0) {
return $products;
}
if (isset($products->_pompoSupplierDeliveryDateIncrementFetched)) {
return $products;
}
$qb = sqlQueryBuilder()
->select('pos.id_supplier, pos.id_product, pos.id_variation, MIN(COALESCE(pos.note, COALESCE(IF(JSON_VALUE(data, \'$.delivery_time\')=\'\', 4, JSON_VALUE(data, \'$.delivery_time\')), 4))) as note')
->from('products_of_suppliers', 'pos')
->join('pos', 'suppliers', 's', 's.id = pos.id_supplier')
->andWhere('pos.in_store > 0')
->groupBy('pos.id_product, pos.id_variation');
$specs = [];
foreach ($products as $key => $product) {
$id = explode('/', (string) $key);
$productId = (int) $id[0];
$variationId = null;
if (!empty($id[1])) {
$variationId = (int) $id[1];
}
$specs[] = Operator::equalsNullable(['id_product' => $productId, 'id_variation' => $variationId]);
}
$qb->andWhere(Operator::orX($specs));
foreach ($qb->execute() as $item) {
$key = $item['id_product'];
if ($item['id_variation']) {
$key .= '/'.$item['id_variation'];
}
if ($products[$key] ?? false) {
$products[$key]->_pompoSupplierId = $item['id_supplier'] ?? null;
$products[$key]->supplierDeliveryDateIncrement = !empty($item['note']) ? (int) $item['note'] : null;
}
}
$products->_pompoSupplierDeliveryDateIncrementFetched = true;
return $products;
}
/**
* Nastaví produktům stitek podle podmínek.
*
* Bezva cena - DMOC sleva >= 16
* Cenový hit - DMOC sleva >= 36
*/
public function generateProductDiscountLabels(): void
{
// Bezva cena
$labelIdBC = $this->labelUtil->getLabelIdByCode('BC');
// Cenový hit
$labelIdCH = $this->labelUtil->getLabelIdByCode('CH');
if (!$labelIdBC || !$labelIdCH) {
return;
}
$productList = clone $this->productList;
$productList->fetchProductOfSuppliersInStore();
$productList->applyDefaultFilterParams();
$productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products));
$batchSize = 500;
$updateData = [
$labelIdBC => [],
$labelIdCH => [],
];
// projdu produkty abych nasel ty, kterym potrebuju nastavit kampan
$iterationLimit = $productList->getProductsCount() / $batchSize;
for ($i = 0; $i < $iterationLimit; $i++) {
$productList->limit($batchSize, $i * $batchSize);
foreach ($productList->getProducts() as $product) {
if (!isset($product->dmocDiscount)) {
continue;
}
if (!$product->dmocDiscount->isPositive()) {
continue;
}
$discount = $product->dmocDiscount->asFloat();
// Cenovy hit
if ($discount >= 36) {
$updateData[$labelIdCH][] = $product->id;
// Bezva cena
} elseif ($discount >= 16) {
$updateData[$labelIdBC][] = $product->id;
}
}
}
foreach ($updateData as $labelId => $data) {
// smazu vygenerovane stitky od produktu
sqlQueryBuilder()
->delete('product_labels_relation')
->where(Operator::equals(['id_label' => $labelId]))
->execute();
$baseInsertQb = sqlQueryBuilder()
->insert('product_labels_relation')
->onDuplicateKeyUpdate(['id_label', 'id_product']);
// nastavim stitek k produktum
foreach (array_chunk($data, 500) as $products) {
$qb = clone $baseInsertQb;
foreach ($products as $productId) {
$qb->multiDirectValues(['id_label' => $labelId, 'id_product' => $productId, 'generated' => 1]);
}
$qb->execute();
}
}
}
public function updateProductsVIPPriceList(): void
{
$selectQb = sqlQueryBuilder()
->select($this->configuration->getVIPPriceListId(), 'p.id', 'ROUND(p.price_buy * 1.05, 4) as vip_price')
->from('products', 'p')
->where('price_buy IS NOT NULL AND price_buy > 0');
sqlQuery("INSERT INTO pricelists_products (id_pricelist, id_product, price)
{$selectQb->getSQL()}
ON DUPLICATE KEY UPDATE price = VALUES(price);");
}
/**
* Nastaví produktům flagy podle podmínek.
*
* Bezva cena - DMOC sleva >= 16
* Cenový hit - DMOC sleva >= 36
*
* @depracated Delete me when labels are fully used instead of campaigns
*/
public function generateProductDiscountFlags(): void
{
$campaigns = getCampaigns();
if (!isset($campaigns['BC']) || !isset($campaigns['CH'])) {
return;
}
$productList = clone $this->productList;
$productList->fetchProductOfSuppliersInStore();
$productList->applyDefaultFilterParams();
$productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products));
$batchSize = 500;
$updateData = [
'BC' => [],
'CH' => [],
];
// projdu produkty abych nasel ty, kterym potrebuju nastavit kampan
$iterationLimit = $productList->getProductsCount() / $batchSize;
for ($i = 0; $i < $iterationLimit; $i++) {
$productList->limit($batchSize, $i * $batchSize);
foreach ($productList->getProducts() as $product) {
if (!isset($product->dmocDiscount)) {
continue;
}
if (!$product->dmocDiscount->isPositive()) {
continue;
}
$discount = $product->dmocDiscount->asFloat();
// Cenovy hit
if ($discount >= 36) {
$updateData['CH'][] = $product->id;
// Bezva cena
} elseif ($discount >= 16) {
$updateData['BC'][] = $product->id;
}
}
}
foreach ($updateData as $campaign => $data) {
// smazu kampan od produktu
sqlQueryBuilder()
->update('products')
->set('campaign', 'REMOVE_FROM_SET(:campaign, campaign)')
->where(Operator::findInSet([$campaign], 'campaign'))
->setParameter('campaign', $campaign)
->execute();
// nastavim kampan k produktum
foreach (array_chunk($data, 500) as $products) {
sqlQueryBuilder()
->update('products')
->set('campaign', 'ADD_TO_SET(:campaign, campaign)')
->where(Operator::inIntArray(array_values($products), 'id'))
->setParameter('campaign', $campaign)
->execute();
}
}
}
/**
* Vrací prodejce indexované podle ID skladu.
*/
private function getSellersByStoreId(): array
{
return Mapping::mapKeys(
Contexts::get(SellerContext::class)->getSupported(),
function ($k, $v) {
return [$v['id_store'], $v];
}
);
}
/**
* Vrací `ProductCollection` podle zadaných ID produktů.
*/
private function getProducts(array $products): ProductCollection
{
static $productsCache = [];
$cacheKey = md5(serialize($products));
if (!($productsCache[$cacheKey] ?? false)) {
$productList = clone $this->productList;
$productList->setVariationsAsResult(true);
$collection = $productList->andSpec(Product::productsAndVariationsIds($products))
->getProducts();
$collection->fetchProductOfSuppliersInStore();
$productsCache[$cacheKey] = $collection;
}
return $productsCache[$cacheKey];
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\User;
use External\PompoBundle\DRS\Synchronizer\UserSynchronizer;
use External\PompoBundle\Email\UserPOSRegistrationEmail;
use External\PompoBundle\Util\PompoLogger;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\DomainContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Util\Discount\DiscountGenerator;
use Query\Operator;
use Symfony\Component\Routing\RouterInterface;
class PompoUserUtil
{
public function __construct(
private UserSynchronizer $userSynchronizer,
private PompoLogger $pompoLogger,
private DiscountGenerator $discountGenerator,
private ContextManager $contextManager,
private UserPOSRegistrationEmail $userPOSRegistrationEmail,
) {
}
public function sendPreregistrationEmail(int $userId, ?int $drsId = null, ?string $email = null, ?string $country = null): void
{
$preregistrationEmail = clone $this->userPOSRegistrationEmail;
$languageMap = [
'CZ' => 'cs',
'SK' => 'sk',
];
$userData = sqlQueryBuilder()
->select('du.id_drs, u.email, u.country')
->from('users', 'u')
->leftJoin('u', 'drs_users', 'du', 'du.id_user = u.id')
->andWhere(Operator::equals(['u.id' => $userId]))
->sendToMaster()
->execute()->fetchAssociative();
$drsId ??= $userData['id_drs'];
$email ??= $userData['email'];
$country ??= $userData['country'] ?? '';
$userLanguage = $languageMap[$country] ?? 'cs';
// potrebuju aktivovat spravnou domenu, kdyz budu posilat mail
$domainContext = Contexts::get(DomainContext::class);
$originalDomain = $domainContext->getActiveId();
$domainContext->activate(
(string) $this->contextManager->getDomainFromLanguage($userLanguage)
);
// aktivuju jazyk podle zeme
$this->contextManager->activateContexts(
[LanguageContext::class => $userLanguage],
function () use ($preregistrationEmail, $email, $drsId) {
$message = $preregistrationEmail->getEmail(
[
'ODKAZ_REGISTRACE' => path('register',
[
'customer' => base64_encode((string) $drsId),
],
RouterInterface::ABSOLUTE_URL
),
]
);
$message['to'] = $email;
// odeslat mail
$preregistrationEmail->sendEmail($message);
});
$domainContext->activate($originalDomain);
addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_SYNC,
sprintf('[DRS] Byl odeslán e-mail na dokončení registrace: %s', $email),
[
'userId' => $userId,
'email' => $email,
'userLanguage' => $userLanguage,
'userCountry' => $country,
]
);
}
/**
* Funkce, ktera uzivatelum generuje narozeninove kupony. Kazdy uzivatel muze mit v uctu pridane
* svoje deti a pokud nejake dite ma narozeniny, tak se vygeneruje narozeninovy poukaz.
*/
public function generateBirthdayDiscountCoupons(): void
{
$currentYear = (new \DateTime())->format('Y');
// selectuju uzivatele, kteri me zajimaji
$qb = sqlQueryBuilder()
->select('u.id, uf.id as id_child, uf.data as child_data, u.id_language, u.country, u.delivery_country, DATE_FORMAT(uf.date_birth, "%m-%d") as child_birth')
->from('users_family', 'uf')
->join('uf', 'users', 'u', 'u.id = uf.id_user')
// musi být mladší než 14 let - radši přičítám 15 dní místo 14, ať je jistota, že mi tam náhodou nevleze
->andWhere('TIMESTAMPDIFF(YEAR, uf.date_birth, CURDATE() + INTERVAL 15 DAY) < 14')
// nesmi mit vygenerovany poukaz
->andWhere('JSON_VALUE(uf.data, \'$.coupons.'.$currentYear.'\') IS NULL')
// podle data narozeni - poukazy generujeme 14 dni dopredu + pripadne pokud je to min jak 14 dnu, tak ho vygenerujeme taky
->andWhere(Operator::inStringArray($this->getDaysParameter(), 'DATE_FORMAT(uf.date_birth, "%m-%d")'))
->orderBy('DATE_FORMAT(uf.date_birth, "%m-%d")')
->groupBy('uf.id');
// tohle je kvuli konci roku, kdy mi dny za 14 dni skoci uz do noveho roku
$nextYearDays = $this->getNextYearDays();
$nextYear = (new \DateTime())->add(new \DateInterval('P1Y'))->format('Y');
$generated = [];
foreach ($qb->execute() as $item) {
// rok kterej kontroluju
$currentYearForCheck = $currentYear;
// Tohle je kvuli konci roku, kdy chci pro lednovy deti generovat poukaz jen jednou - abych ho jednou nevygeneroval v jednom roce
// a po druhe zase v novym roce
if (in_array($item['child_birth'], $nextYearDays)) {
$currentYearForCheck = $nextYear;
}
$childData = json_decode($item['child_data'] ?? '', true) ?? [];
// safe check
if ($childData['coupons'][$currentYearForCheck] ?? false) {
continue;
}
// podle zeme uzivatele si nastavim jazyk
$language = 'cs';
if ($item['id_language'] === 'sk' || $item['country'] === 'SK' || $item['delivery_country'] === 'SK') {
$language = 'sk';
}
// id generovaneho kodu, pro ktery budu kupon generovat - rozlisujeme CZ a SK kupony kvuli mene
if (!($discountId = $this->getBirthdayDiscountIdByLanguage($language))) {
continue;
}
// vygeneruju kupon
$result = $this->discountGenerator->generateNewCoupons(id_discount: $discountId, userId: (int) $item['id']);
$generated[$language] = ($generated[$language] ?? 0) + $result['count'];
// ulozim si k diteti, ze tenhle rok uz poukaz dostal, takze nebudu generovat dalsi
$childData['coupons'][$currentYearForCheck] = reset($result['coupons']);
sqlQueryBuilder()
->update('users_family')
->directValues(
[
'data' => json_encode($childData),
]
)
->where(Operator::equals(['id' => $item['id_child']]))
->execute();
}
// zaloguju zpravu do activitylogu o tom, ze jsem vygeneroval poukazy
if (!empty($generated) && max($generated) > 0) {
$generatedText = [];
foreach ($generated as $lang => $count) {
$generatedText[] = $lang.'='.$count;
}
addActivityLog(
ActivityLog::SEVERITY_SUCCESS,
ActivityLog::TYPE_SYNC,
sprintf('Byly vygenerovány narozeninové poukazy: %s', implode(';', $generatedText)),
$generated
);
}
}
/**
* Pokud se uzivatel registroval na prodejne a pak prisel an e-shop a zvolil, ze se registroval na prodejne
* a timpadem paruje uz existujici ucet (kvuli bodum ve vernostim programu) tak ho hned po registraci sesynchronizuju.
*/
public function synchronizeRegisteredUser(\User $user): void
{
try {
if (!($customerId = ($user->getCustomData()['forceCustomerSynchronization'] ?? false))) {
return;
}
sqlGetConnection()->transactional(function () use ($customerId, $user) {
// Vytvorim mapping
sqlQueryBuilder()
->insert('drs_users')
->directValues(
[
'id_drs' => $customerId,
'id_user' => $user->id,
]
)->execute();
// Sesynchronizuju karty a deti
$this->userSynchronizer->updateUser(
[
'id' => (int) $customerId,
'email' => $user->email,
],
[
'cards' => true,
'children' => true,
]
);
});
} catch (\Throwable $e) {
$this->pompoLogger->logException($e, 'Selhala synchronizace uživatele při registraci přes prodejnu.');
}
}
/**
* Pomocná funkce při generování poukazů.
* Vrací pole s datumama dnů ode dneška až po den za 14 dní. Ve formátu m-d, protože mě při generování poukazů zajíma jen měsíc a den.
*/
private function getDaysParameter(): array
{
$days = [];
foreach ($this->getDatePeriod() as $date) {
$days[] = $date->format('m-d');
}
return $days;
}
/**
* Pomocná funkce, která vrací mezi dneškem a datem za 14 dnů všechny dny, které jsou v dalším roce.
* Používá se při generování poukazů abych správně generoval u konce roku.
*/
private function getNextYearDays(): array
{
$currentYear = (new \DateTime())->format('Y');
$days = [];
foreach ($this->getDatePeriod() as $date) {
if ($date->format('Y') === $currentYear) {
continue;
}
$days[] = $date->format('m-d');
}
return $days;
}
/**
* Vrací datumy ode dneška až po datum za 14 dní.
*/
private function getDatePeriod(): \DatePeriod
{
return new \DatePeriod(
new \DateTime(),
new \DateInterval('P1D'),
(new \DateTime())->add(new \DateInterval('P14D'))
);
}
/**
* Vrací ID generovaného poukazu podle jazyka.
*/
private function getBirthdayDiscountIdByLanguage(string $language): ?int
{
$map = [
'cs' => 159,
'sk' => 162,
];
return $map[$language] ?? null;
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\User;
use Query\Operator;
class UserCardUtil
{
/** Délka kódu karty, kterou generujeme */
private const CARD_LENGTH = 9;
/**
* Vrací všechny věrnostní kartičky daného uživatele.
*/
public function getUserCards(int $userId): array
{
$cards = [];
$qb = sqlQueryBuilder()
->select('uc.*')
->from('user_cards', 'uc')
->where(Operator::equals(['id_user' => $userId]));
foreach ($qb->execute() as $item) {
$item['data'] = json_decode($item['data'] ?? '', true) ?? [];
$cards[$item['id']] = $item;
}
return $cards;
}
/**
* Vygeneruje novou věrnostní kartu k danému uživateli.
*/
public function generateUserCard(int $userId): void
{
sqlQueryBuilder()
->insert('user_cards')
->directValues(
[
'id_user' => $userId,
'code' => $this->generateCardCode(),
]
)->execute();
}
public function generateCardCode(): string
{
$codeAlphabet = 'ABCDEFGHJKLMNPRSTUVWX';
$codeAlphabet .= '23456789';
$max = strlen($codeAlphabet);
$token = '';
for ($i = 0; $i < self::CARD_LENGTH; $i++) {
$token .= $codeAlphabet[$this->cryptoRandSecure($max - 1)];
}
return implode('-', str_split($token, 3));
}
private function cryptoRandSecure(int $max): int
{
$min = 0;
$range = $max - $min;
if ($range < 1) {
return $min;
}
$log = ceil(log($range, 2));
$bytes = (int) ($log / 8) + 1; // length in bytes
$bits = (int) $log + 1; // length in bits
$filter = (int) (1 << $bits) - 1; // set all lower bits to 1
do {
$rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
$rnd = $rnd & $filter; // discard irrelevant bits
} while ($rnd > $range);
return $min + $rnd;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace External\PompoBundle\Util\Watchdog;
use External\PompoBundle\DataGo\Synchronizer\StockSynchronizer;
use KupShop\WatchdogBundle\Util\Watchdog;
use Query\QueryBuilder;
class PompoWatchdog extends Watchdog
{
/**
* Předefinovávám `getInStoreField`, protože chci vracet skladovost pouze pro centrální sklad. Pokud je produkt skladem třeba
* jen na jedný prodejně, tak to zákazníka nemusí zajímat, protože ta prodejna může být na druhý straně republiky.
*/
public function getInStoreField(QueryBuilder $qb, string $productAlias = 'p', string $variationAlias = 'pv'): string
{
// Najoinuju si skladovost na centralnim skladu
$qb->leftJoin($productAlias, 'stores_items', 'si_wd', "si_wd.id_product = {$productAlias}.id AND {$variationAlias}.id IS NULL AND si_wd.id_store = :wdStoreId")
->setParameter('wdStoreId', StockSynchronizer::STORE_ID);
// Vracim skladovost na centralnim skladu
return 'COALESCE(si_wd.quantity, 0)';
}
}