Files
kupshop/bundles/External/PompoBundle/DRS/Synchronizer/UserSynchronizer.php
2025-08-02 16:30:27 +02:00

1129 lines
40 KiB
PHP

<?php
declare(strict_types=1);
namespace External\PompoBundle\DRS\Synchronizer;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use External\PompoBundle\Email\UserPOSRegistrationEmail;
use External\PompoBundle\Util\Configuration;
use External\PompoBundle\Util\User\PompoUserUtil;
use External\PompoBundle\Util\User\UserCardUtil;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\StringUtil;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
/**
* Synchronizace uzivatelu do DRSu, zpetne z DRSu stahujeme adresy, karty a deti.
*/
class UserSynchronizer extends AbstractSynchronizer
{
#[Required]
public PompoUserUtil $pompoUserUtil;
#[Required]
public UserCardUtil $userCardUtil;
#[Required]
public Configuration $configuration;
#[Required]
public UserPOSRegistrationEmail $registrationEmail;
#[Required]
public ContextManager $contextManager;
protected static $type = 'user';
private ?int $timestampSql = null;
private ?int $timestampLocal = null;
private const USER_DELETED_NOTE = 'Uživatel smazán v DRSu';
private const USER_GROUP_EMPLOYEE = 10247;
private const MAX_RESULT_ITERATION = 10_000;
public function process(): void
{
$this->processFromDRS();
$this->timestampLocal = time();
$this->processToDRS();
$this->processBonusPointsToDRS();
$this->updateTimestamps();
}
/**
* Zapisovani novych uzivatelu a aktualizace upravenych uzivatelu na e-shopu do DRSu.
*/
public function processToDRS(): void
{
if (isDevelopment()) {
return;
}
$timestamp = $this->getUserTimestamp()['local'] ?? null;
$qb = sqlQueryBuilder()
->select('u.*, du.id_drs')
->from('users', 'u')
->leftJoin('u', 'drs_users', 'du', 'du.id_user = u.id')
->andWhere('u.date_reg IS NOT NULL')
->groupBy('u.id');
if ($timestamp) {
// uzivatel byl aktualizovan - provedla se na nem nejaka zmena, takze ho poslu do DRSu
$qb->andWhere('u.date_updated IS NOT NULL AND u.date_updated >= :lastSync')
->setParameter('lastSync', date('Y-m-d H:i:s', $timestamp));
$qb->setMaxResults(self::MAX_RESULT_ITERATION);
}
foreach ($qb->execute() as $item) {
$customerId = $item['id_drs'];
if ($customerId && str_contains($item['email'], $customerId) && str_contains($item['email'], 'pompo.cz')) {
continue;
}
$this->debugLogUserChange((int) $item['id'], 'processToDRS::start', [
'timestamp' => $timestamp,
'item' => $item,
'drsId' => $customerId,
]);
// Zamestnanecky ucet - nechceme ho v DRSu aktualizovat, abychom jim neprepisovali nejaky jejich data.
// 5 cislic a zacina cislem 6
// NEBO pokud byl uzivatel "smazan" z DRSu, tak ho uz do DRSu nesynchronizujeme
if ($this->isUserEmployer($customerId ? (int) $customerId : null) || $item['note'] === self::USER_DELETED_NOTE) {
$this->debugLogUserChange((int) $item['id'], 'processToDRS::skip', [
'isUserDeleted' => $item['note'] === self::USER_DELETED_NOTE,
'isUserEmployer' => $this->isUserEmployer($customerId ? (int) $customerId : null),
]);
continue;
}
$user = new \User();
$user->loadData($item);
if (empty($customerId)) {
// zkusim najit uzivatele v DRSu, abych ho pripadne nezakladal znovu
if ($customer = $this->drsApi->getUserByEmail($user->email)) {
if ($customerId = $customer['@attributes']['contactNumber'] ?? null) {
$drsCustomer = $this->drsApi->getSQLUserByContactNumber((int) $customerId);
// pokud je ten uzivatel v DRSu smazanej, tak uz ho nejspis i mame na shopu, takze to na nej
// nechceme parovat, protoze nedava smysl to parovat na smazanej ucet
// takze nastavim $customerId na null, aby se vytvoril novej uzivatel v DRSu
if ($drsCustomer && $this->isUserDeletedInDRS($drsCustomer)) {
$customerId = null;
}
}
}
}
$userData = $this->getUserData($user, $customerId ? (int) $customerId : null);
// Ulozim / updatuju uzivatele v DRSu
$customerId = $this->saveUser(
$userData
);
$this->debugLogUserChange((int) $item['id'], 'processToDRS::saveUser', [
'saveUserResult' => $customerId,
'userData' => $userData,
]);
// Vytvorim si mapping, pokud jeste neexistoval
if ($customerId && empty($item['id_drs'])) {
$this->createUserMapping((int) $user->id, $customerId);
}
}
}
/**
* Aktualizace bodu smerem z e-shopu do DRSu.
*
* Oddeleno od synchronizace uzivatelu do DRSu kvuli problemum, ktere to zpusobovalo. Pri aktualizaci bodu nechceme
* aktualizovat e-mail, primarne chceme aktualizovat pouze ty body, ale bohuzel je potreba posilat i dalsi udaje.
*/
public function processBonusPointsToDRS(?int $timestamp = null): void
{
if (isDevelopment()) {
return;
}
$timestamp = $timestamp ?: ($this->getUserTimestamp()['local'] ?? null);
$qb = sqlQueryBuilder()
->select('u.*, du.id_drs')
->from('users', 'u')
->join('u', 'drs_users', 'du', 'du.id_user = u.id')
->andWhere('u.date_reg IS NOT NULL')
->groupBy('u.id');
if ($timestamp) {
// byla provedena nejaka zmena na bodech uzivatele, takze ho chci poslat do DRSu, aby se mu aktualizovali body
$qb->andWhere('(u.id IN (SELECT bp.id_user FROM bonus_points bp WHERE bp.date_created >= :lastSync GROUP BY bp.id_user))')
->setParameter('lastSync', date('Y-m-d H:i:s', $timestamp));
$qb->setMaxResults(self::MAX_RESULT_ITERATION);
}
foreach ($qb->execute() as $item) {
$customerId = (int) $item['id_drs'];
if ($this->isUserEmployer($customerId) || $item['note'] === self::USER_DELETED_NOTE) {
continue;
}
$points = $this->getUserBonusPoints((int) $item['id']);
try {
$this->drsApi->updateUserBonusPoints($customerId, $points);
} catch (\Throwable $e) {
$this->logger->logException($e, sprintf('[DRS] Aktualizace bodů z e-shopu do DRSu se nezdařila: %s', $customerId), [
'points' => $points,
'drsId' => $customerId,
]);
}
$this->debugLogUserChange((int) $item['id'], 'processBonusPointsToDRS', [
'drsId' => $customerId,
'points' => $points,
]);
}
}
/**
* Stahovani zmen z DRSu k uzivateli na e-shopu.
*/
public function processFromDRS(?int $timestamp = null, ?int $limit = 1_000): void
{
if (!$timestamp) {
$timestamp = $this->getUserTimestamp()['sql'] ?? null;
}
$this->debugLogUserChange(0, 'processFromDRS::start', []);
$iteration = 0;
// ziskam si uzivatele, u kterych se neco zmenilo
// nacitam max `$limit` uzivatelu v ramci jednoho sync runu, abych nebloknul sync v pripade sileneho mnozstvi zmenenych uzivatelu
$users = $this->drsApi->getUsersWithAdditionalData(
timestamp: $timestamp,
limit: $limit,
onBatchProcessed: function () {
// aktualizovat sql timestamp v settingach po tom, co se zpracuje batch, aby se to prubezne posouvalo
$this->updateTimestamps(local: false);
}
);
foreach ($users as $item) {
$this->debugLogUserChange(0, 'processFromDRS::processing', ['item' => $item]);
try {
$this->updateUser($item);
} catch (\SoapFault|UniqueConstraintViolationException $e) {
// ignoruju SOAP chyby a duplicity
}
// ulozit timestamp, kterej se pak ulozi do DB
$this->timestampSql = max((int) $item['timestamp'], $this->timestampSql ?? (int) $item['timestamp']);
if ($this->mode !== self::MODE_FULL) {
if ($iteration > self::MAX_RESULT_ITERATION) {
break;
}
$iteration++;
}
}
// zaktivovat uzivatele, kterym se doplnil mail
$this->updateUsersVisibility();
}
public function processDRSUser(int $drsId): void
{
if ($user = $this->drsApi->getUser($drsId)) {
$this->updateUser($user);
}
}
public function updateUser(array $item, array $config = []): void
{
try {
// nactu si uzivatele pres SOAP, protoze tam jsou data, ktere potrebuji a je tezsi se k nim dostat jinak
if (!($customer = $this->drsApi->getUserById((int) $item['id']))) {
return;
}
} catch (\SoapFault $e) {
// nejspis se jedna o smazaneho uzivatele, a tak ho nejde pres SOAP nacist, ale potrebuju mu udelat update na shopu i tak
if (!str_contains($e->getMessage(), 'Odkaz na objekt není nastaven na instanci objektu')) {
throw $e;
}
$customer = [];
}
$item['attributes'] = Mapping::mapKeys(
json_decode(($item['attributes'] ?? null) ?: '', true) ?: [],
fn ($k, $v) => [$v['name'], $v['value'] ?? null]
);
$deleted = $this->isUserDeletedInDRS($item);
// pokud uzivatel neexistuje, tak ho zkusim zalozit
if (!($userId = $this->getUserIdByDRSId($item['id']))) {
// pokud je smazanej, tak ho ani nezakladam
if ($deleted) {
return;
}
// nemam data podle kterych bych mo mel zalozit
if (empty($customer)) {
return;
}
// Pokud na eshopu uzivatel neexistuje, tak ho vytvorim
if (!($userId = $this->createDRSUser($customer))) {
return;
}
}
// pokud uzivatele v DRSu smazali, tak ho "odstanim" i u nas
if ($deleted) {
$this->updateDeletedUser($userId, $item);
$this->updateUserRelatedData($userId, $item, $customer, $config);
return;
}
$this->debugLogUserChange($userId, 'updateUser', [
'databaseUser' => $item,
'apiUser' => $customer,
]);
$update = ['get_news' => ($customer['@attributes']['wantsEmails'] ?? false) === 'True' ? 'Y' : 'N'];
if (!empty($item['email']) && filter_var($item['email'], FILTER_VALIDATE_EMAIL)) {
$update['email'] = $item['email'];
}
// aktualizace mailu a newsletteru
$this->updateUserEmail($userId, (int) $item['id'], $update, (string) ($item['Country'] ?? ''));
// aktualizace skupin uzivatele
$this->updateUserGroups($userId, $item);
// aktualizace fakturacnich udaju
$this->updateUserDRSAddress($userId, [
'firstName' => $item['FirstName'] ?? '',
'lastName' => $item['LastName'] ?? '',
'street' => $item['Street'] ?? '',
'city' => $item['City'] ?? '',
'zipCode' => $item['Zip'] ?? '',
'phone' => html_entity_decode($item['Phone'] ?? ''),
'country' => $item['Country'] ?? '',
]);
// aktualizace dodaci adresy
// vypinam synchronizaci dodaci adresy, protoze se nam ji stejnak nedari zapisovat tak, jak bychom potrebovali
// a akorat pak v uctu na e-shopu mizi zakaznicke adresy, viz. https://klient.wpj.cz/service/task/8390/change/
// kde jsem to resil...
// if (empty($config) || ($config['addresses'] ?? false) === true) {
// foreach ($this->getItemsAsIndexedArray($customer['address'] ?? []) as $address) {
// $address = $address['@attributes'] ?? [];
// // adresa neni aktivni
// if ($address['status'] != 1 || $address['addressType'] != 1) {
// continue;
// }
//
// $this->updateUserDRSAddress($userId, $address, 'delivery');
// // pouze delivery adresa - vic by jich tam ani byt nemelo
// break;
// }
// }
// aktualizace zakaznicnich karet a deti
$this->updateUserRelatedData($userId, $item, $customer, $config);
}
/**
* Provedu "smazani" uzivatele v e-shopu.
*/
public function updateDeletedUser(int $userId, array $customer): void
{
sqlQueryBuilder()
->update('users')
->directValues(
[
'email' => $customer['id'].'@pompo.cz',
'figure' => 'N',
'note' => self::USER_DELETED_NOTE,
]
)
->where(Operator::equals(['id' => $userId]))
->execute();
}
private function updateUserGroups(int $userId, array $item): void
{
sqlGetConnection()->transactional(function () use ($userId, $item) {
sqlQueryBuilder()
->delete('users_groups_relations')
->andWhere(Operator::equals(['id_user' => $userId]))
->andWhere(Operator::inIntArray([self::USER_GROUP_EMPLOYEE], 'id_group'))
->execute();
if (($item['attributes']['IsEmployee'] ?? false) === 'True') {
sqlQueryBuilder()
->insert('users_groups_relations')
->directValues(['id_user' => $userId, 'id_group' => self::USER_GROUP_EMPLOYEE])
->execute();
}
});
}
private function updateUserRelatedData(int $userId, array $item, array $customer, array $config): void
{
// aktualizace zakaznicnich karet
if (empty($config) || ($config['cards'] ?? false) === true) {
$this->updateUserCards(
$userId,
$this->getItemsAsIndexedArray($customer['customercard'] ?? [])
);
}
// aktualizace deti - na prodejne si je uzivatel schopen napr. pridat dite
if (empty($config) || ($config['children'] ?? false) === true) {
$this->updateUserChildren(
$userId,
$this->prepareChildrenFromDatabaseUser($item)
);
}
}
private function updateUserCards(int $userId, array $cards): void
{
// aktualizace zakaznickych karet
$currentCards = array_map(fn ($x) => $x['code'], QueryHint::withRouteToMaster(fn () => $this->userCardUtil->getUserCards($userId)));
// karty, ktere budu na konci mazat
$cardsToDelete = $currentCards;
foreach ($cards as $card) {
$card = $card['@attributes'] ?? [];
// pokud by nemela cislo karty, tak skipuju
if (empty($card['number'])) {
continue;
}
// pokud karta uz existuje, tak nic nedelam a jen ji odeberu z karet ke smazani
if (($cardId = array_search($card['number'], $currentCards)) !== false) {
unset($cardsToDelete[$cardId]);
continue;
}
if (($card['status'] ?? false) !== '1') {
continue;
}
try {
$this->createUserCard($userId, $card['number']);
} catch (UniqueConstraintViolationException) {
}
}
if (!empty($cardsToDelete)) {
sqlQueryBuilder()
->delete('user_cards')
->andWhere(Operator::equals(['id_user' => $userId]))
->andWhere(Operator::inIntArray(array_keys($cardsToDelete), 'id'))
->execute();
}
}
private function updateUserChildren(int $userId, array $children): void
{
sqlGetConnection()->transactional(function () use ($children, $userId) {
$qb = sqlQueryBuilder()
->select('*')
->from('users_family')
->where(Operator::equals(['id_user' => $userId]));
$childrenByName = [];
// pred smazanim si deti ulozim, abych mohl prenest custom data
foreach ($qb->execute() as $item) {
$childrenByName[trim(mb_strtolower($item['name']))] = $item;
}
// smazu vsechny deti, abych je mohl znovu naimportovat - tim se provede update
sqlQueryBuilder()
->delete('users_family')
->where(Operator::equals(['id_user' => $userId]))
->execute();
// poukladam deti k nam do tabulky
foreach ($children as $child) {
sqlQueryBuilder()
->insert('users_family')
->directValues(
[
'id_user' => $userId,
'name' => $child['ChildName'],
'gender' => !empty($child['ChildSex']) ? $child['ChildSex'] == 1 ? 'M' : 'F' : null,
'date_birth' => $child['ChildBirthDay'] ?? null,
'data' => $childrenByName[trim(mb_strtolower($child['ChildName']))]['data'] ?? null,
]
)->execute();
}
});
}
private function createUserCard(int $userId, string $cardNumber, bool $deep = false): void
{
try {
sqlQueryBuilder()
->insert('user_cards')
->directValues(
[
'id_user' => $userId,
'code' => $cardNumber,
]
)->execute();
} catch (UniqueConstraintViolationException $e) {
if ($deep) {
throw $e;
}
sqlQueryBuilder()
->delete('user_cards')
->where(Operator::equals(['code' => $cardNumber]))
->execute();
$this->createUserCard($userId, $cardNumber, true);
}
}
/**
* Vraci data, ktere se posilaji pro aktualizaci uzivatele v DRSu.
*/
public function getUserData(\User $user, ?int $customerId): array
{
$user->fetchAddresses();
$userBonusPoints = $this->getUserBonusPoints((int) $user->id);
$extendedValue = array_filter([
// datum vytvoreni zakaznika
$this->getExtendedValueItem('CreatedDate', $user->date_reg ?? ''),
// odkud byl zakaznik vytvoren - pokud uz existuje, tak CreatedBranch nechci aktualizovat
$customerId ? null : $this->getExtendedValueItem('CreatedBranch', $this->getCreatedBranch($user)),
// ico
$this->getExtendedValueItem('ITIN', $user->invoice['ico'] ?? ''),
// dic
$this->getExtendedValueItem('VATID', $user->invoice['dic'] ?? ''),
]);
return [
'contact' => [
'@attributes' => [
'contactNumber' => $customerId ?: '',
'loginId' => $user->email,
'email' => $user->email,
'branch' => $this->getCreatedBranch($user),
'firstName' => $user->invoice['name'] ?? '',
'lastName' => $user->invoice['surname'] ?? '',
'bonusPoints' => $userBonusPoints,
'wantsEmails' => ($user->get_news ?? null) === 'Y' ? 'true' : 'false',
'telephone' => $user->invoice['phone'] ?? '',
'street' => $this->substr($user->invoice['street'] ?? ''),
'city' => $this->substr($user->invoice['city'] ?? ''),
'zipCode' => $this->substr($user->invoice['zip'] ?? '', 10),
'state' => $user->invoice['country'] ?: 'CZ',
'country' => $user->invoice['country'] ?: 'CZ',
'sexId' => $this->getUserSexId($user, true),
'discount' => '',
],
'mainVariables' => [],
// adresy uzivatele
'address' => $this->getUserAddresses($user),
// karty uzivatele
'customercard' => $this->getUserCards($user),
'extendedvalue' => array_merge($extendedValue, $this->getUserChildren($user)),
],
];
}
protected function processItem(array $item): void
{
// TODO: Implement processItem() method.
}
/**
* Zalozeni / aktualizace uzivatele v DRSu.
*/
private function saveUser(array $data): ?int
{
try {
return $this->drsApi->saveUser($data);
} catch (\SoapFault $e) {
if (isDevelopment()) {
throw $e;
}
// ignoruju SoapFault, protoze ten jejich SOAP strasne casto failuje
}
return null;
}
/**
* Vytvoreni uzivatele v e-shopu.
*/
private function createDRSUser(array $item): ?int
{
// safe-check
if (!($customerId = ($item['@attributes']['contactNumber'] ?? null))) {
return null;
}
$insert = [
'email' => $item['@attributes']['email'] ?? null,
'figure' => 'N',
'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'),
'id_language' => null,
];
if (empty($insert['email']) || !filter_var($insert['email'], FILTER_VALIDATE_EMAIL)) {
$insert['email'] = $customerId.'@pompo.cz';
}
if ($recTimeStamp = ($item['@attributes']['recTimeStamp'] ?? null)) {
if ($recTimeStamp = \DateTime::createFromFormat('Y-m-d\TH:i:s', $recTimeStamp)) {
$insert['date_reg'] = $recTimeStamp->format('Y-m-d H:i:s');
}
}
// Uzivatele uz muzeme mit v e-shopu, protoze si mohl prihlasit newsletter
$newsletterUser = $this->getNewsletterUser($insert['email']);
return sqlGetConnection()->transactional(function () use ($newsletterUser, $customerId, $insert) {
// Pokud mam uzivatele, kterej se vytvoril pres newsletter
if ($newsletterUser) {
// Prenesu nastaveni newsletteru na uzivatele, ktereho budu vytvaret
$insert['get_news'] = $newsletterUser['get_news'];
$insert['date_subscribe'] = $newsletterUser['date_subscribe'];
$insert['date_unsubscribe'] = $newsletterUser['date_unsubscribe'];
// Smazu toho newsletter uzivatele, abych mohl zalozit uzivatele podle DRSu
sqlQueryBuilder()
->delete('users')
->andWhere('date_reg IS NULL')
->andWhere(Operator::equals(['id' => $newsletterUser['id']]))
->execute();
}
// zalozim uzivatele
sqlQueryBuilder()
->insert('users')
->directValues($insert)
->execute();
$userId = (int) sqlInsertId();
// vytvorim mapping
sqlQueryBuilder()
->insert('drs_users')
->directValues(
[
'id_drs' => $customerId,
'id_user' => $userId,
]
)->execute();
return $userId;
});
}
/**
* Zaktivuje neaktivni uzivatele, pokud to jsou registrovani uzivatele a maji vyplneny mail.
*
* Pokud se uzivatel totiz zaklada z DRSu, tak ma vetsinou automaticky generovany mail {drs_id}@pompo.cz
* takze kdyz se pak pozdeji zaktualizuje, tak zustava furt neaktivni. Takovyhle uzivatele prave tady zviditelnime.
*/
private function updateUsersVisibility(): void
{
sqlQueryBuilder()
->update('users', 'u')
->join('u', 'drs_users', 'du', 'du.id_user = u.id')
->directValues(['figure' => 'Y'])
->andWhere('u.date_reg IS NOT NULL')
->andWhere(Operator::equals(['u.figure' => 'N']))
->andWhere('u.email NOT LIKE CONCAT("%", du.id_drs, "%")')
->execute();
}
/**
* Pokud se uzivateli v DRSu zmeni e-mail, tak ho prozenu touhle metodou.
*/
private function updateUserEmail(int $userId, int $drsId, array $update, string $country, bool $deep = false): void
{
$this->debugLogUserChange($userId, 'updateUserEmail::start', [
'drsId' => $drsId,
'updateData' => $update,
'deep' => $deep,
]);
// Nacte si aktualni email u uzivatele
$currentEmail = sqlQueryBuilder()
->select('email')
->from('users')
->where(Operator::equals(['id' => $userId]))
->execute()->fetchOne();
// aktualizace mailu a newsletteru
$updated = false;
try {
sqlQueryBuilder()
->update('users')
->directValues($update)
->where(Operator::equals(['id' => $userId]))
->execute();
$this->debugLogUserChange($userId, 'updateUserEmail::updated', [
'updateData' => $update,
'updatedUserEmail' => true,
]);
$updated = true;
} catch (UniqueConstraintViolationException $e) {
if (!$deep) {
// Pokud se uzivateli snazim zmenit email podle DRSu, ale spadne to, protoze v shopu existuje
// newsletter uzivatel, tak potrebuju newsletter uzivatele prenest k aktualnimu uzivateli a
// newsletter uzivatele smazat
if ($newsletterUser = $this->getNewsletterUser($update['email'])) {
sqlGetConnection()->transactional(function () use ($userId, $drsId, $country, $update, $newsletterUser) {
// Prenesu nastaveni newsletteru na uzivatele, ktereho aktualizuji
$update['get_news'] = $newsletterUser['get_news'];
$update['date_subscribe'] = $newsletterUser['date_subscribe'];
$update['date_unsubscribe'] = $newsletterUser['date_unsubscribe'];
// Smazu toho newsletter uzivatele, abych mohl zalozit uzivatele podle DRSu
sqlQueryBuilder()
->delete('users')
->andWhere('date_reg IS NULL')
->andWhere(Operator::equals(['id' => $newsletterUser['id']]))
->execute();
$this->updateUserEmail($userId, $drsId, $update, $country, true);
});
return;
}
}
addActivityLog(
ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
sprintf('[DRS] Nepodařilo se zaktualizovat e-mail pro uživatele: %s', $userId),
[
'userId' => $userId,
'email' => $update['email'] ?? null,
'update' => $update,
]
);
}
// Pokud se provedla uspesne aktualizace, zmenil se email a uzivatel mel predtim docasny email, tak odesleme mail
// aby si uzivatel mohl dokoncit registraci
if ($updated && !empty($update['email']) && preg_match('/^(\d+)@pompo\.cz$/',
$currentEmail) && $currentEmail !== $update['email'] && filter_var($update['email'], FILTER_VALIDATE_EMAIL)) {
$this->pompoUserUtil->sendPreregistrationEmail(
userId: $userId,
drsId: $drsId,
email: $update['email'],
country: !empty($country) ? $country : null,
);
}
}
/**
* Uzivatele uz muzeme mit v e-shopu, protoze si mohl prihlasit newsletter.
*/
private function getNewsletterUser(string $email): ?array
{
$shopUser = sqlQueryBuilder()
->select('*')
->from('users')
// pokud ma jen newsletter, tak nema datum registrace
->andWhere('date_reg IS NULL')
// selectuju podle mailu
->andWhere(Operator::equals(['email' => $email]))
->execute()->fetchAssociative();
if (!$shopUser) {
return null;
}
return $shopUser;
}
private function createUserMapping(int $userId, int $drsId, bool $deep = false): void
{
try {
sqlQueryBuilder()
->insert('drs_users')
->directValues(
[
'id_drs' => $drsId,
'id_user' => $userId,
]
)->execute();
} catch (UniqueConstraintViolationException $e) {
if ($deep) {
throw $e;
}
// mapping uz existuje
// pokud maping existuje pro neaktivniho uzivatele, tak mapping smazu a vytvorim znovu na novem uzivateli
$inactiveUser = sqlQueryBuilder()
->select('u.id')
->from('users', 'u')
->join('u', 'drs_users', 'du', 'du.id_user = u.id')
->where(
Operator::equals(
[
'du.id_drs' => $drsId,
'u.figure' => 'N',
]
)
)->execute()->fetchOne();
if ($inactiveUser) {
sqlQueryBuilder()
->delete('drs_users')
->where(Operator::equals(['id_drs' => $drsId]))
->execute();
$this->createUserMapping($userId, $drsId, true);
}
}
}
/** Zkontroluje, zda uzivatele v DRSu "smazali" */
private function isUserDeletedInDRS(array $item): bool
{
// do FirstName davaji prefix DEL_<ContactNumber>, kde ContactNumber je zakaznik, se kterym ho treba sloucili
if (StringUtil::startsWith($item['FirstName'], 'DEL_')) {
return true;
}
return false;
}
/** Overi, zda se jedna o zamestnanecky ucet */
private function isUserEmployer(?int $customerId): bool
{
return $customerId && strlen((string) $customerId) == 5 && StringUtil::startsWith((string) $customerId, '6');
}
/**
* Aktualizuje adresu uzivatele na e-shopu podle adresy v DRSu.
*/
private function updateUserDRSAddress(int $userId, array $address, string $type = 'invoice'): void
{
$prefix = '';
if ($type === 'delivery') {
$prefix = $type.'_';
}
$update = [
$prefix.'name' => $address['firstName'] ?? '',
$prefix.'surname' => $address['lastName'] ?? '',
$prefix.'firm' => $address['company'] ?? '',
$prefix.'street' => $address['street'] ?? '',
$prefix.'city' => $address['city'] ?? '',
$prefix.'zip' => $address['zipCode'] ?? '',
$prefix.'country' => $address['country'] ?? 'CZ',
];
if (!empty($address['phone'])) {
$update[$prefix.'phone'] = $address['phone'];
}
sqlQueryBuilder()
->update('users')
->directValues($update)
->where(Operator::equals(['id' => $userId]))
->execute();
}
private function getExtendedValueItem(string $name, string $value): array
{
return [
'@attributes' => [
'name' => $name,
'value' => $value,
],
];
}
private function getUserAddresses(\User $user): array
{
$result = [];
// delivery adressu nezapisujeme, protoze to delalo akorat problemy, takze zapisujeme a pracujeme pouze s
// fakturacni adresou, viz. https://klient.wpj.cz/service/task/8390/change/, kde se odmazavali adresy
foreach (['invoice'] as $type) {
$result[] = $this->getUserAddress($user, $type);
}
return $result;
}
private function getUserAddress(\User $user, string $type = 'invoice'): array
{
return [
'@attributes' => [
'addressType' => '1',
'email' => $user->email,
'firstName' => $user->{$type}['name'] ?? '',
'lastName' => $user->{$type}['surname'] ?? '',
'company' => $this->substr($user->{$type}['firm'] ?? '', 33),
'street' => $this->substr($user->{$type}['street'] ?? ''),
'city' => $this->substr($user->{$type}['city'] ?? ''),
'zipCode' => $this->substr($user->{$type}['zip'] ?? '', 10),
'state' => $user->{$type}['country'] ?: 'CZ',
'country' => $user->{$type}['country'] ?: 'CZ',
'houseNumber' => '',
'orientNumber' => '',
'status' => '1',
],
];
}
/**
* Vraci ID shopu, kde byl uzivatel zalozeny.
*/
private function getCreatedBranch(\User $user): string
{
if ($this->configuration->isNejhracka()) {
if ($user->id_language === 'sk') {
return '404';
}
return '403';
}
if ($user->id_language === 'sk') {
return '402';
}
return '401';
}
private function getUserCards(\User $user): array
{
$result = [];
$userCards = QueryHint::withRouteToMaster(fn () => $this->userCardUtil->getUserCards($user->id));
foreach ($userCards as $item) {
$result[]['@attributes'] = [
'number' => $item['code'],
'status' => $item['active'] == 1 ? '1' : '0',
];
}
return $result;
}
private function getUserChildren(\User $user): array
{
$indexMap = $this->getChildrenIndexMap();
$qb = sqlQueryBuilder()
->select('*')
->from('users_family')
->where(Operator::equals(['id_user' => $user->id]))
->orderBy('id', 'ASC')
->sendToMaster();
$result = [];
foreach ($qb->execute() as $index => $item) {
$prefix = $indexMap[$index] ?? '';
$result[]['@attributes'] = [
'name' => $prefix.'ChildBirthDay',
'value' => $item['date_birth'],
];
$result[]['@attributes'] = [
'name' => $prefix.'ChildName',
'value' => $item['name'],
];
$result[]['@attributes'] = [
'name' => $prefix.'ChildSex',
'value' => $item['gender'] == 'M' ? '1' : '0',
];
}
return $result;
}
private function getUserBonusPoints(int $userId): int
{
return (int) sqlQueryBuilder()
->select('SUM(points)')
->from('bonus_points')
->where(\Query\Operator::equals(['id_user' => $userId, 'status' => 'active']))
->execute()->fetchOne() ?? 0;
}
private function getUserSexId(\User $user, bool $negativeSex = false): string
{
// Co se týče pohlaví, tak v TPOMM je nějaký šotek, kdy v nějakých verzích je to 1 - muž a 2 - žena a někde je to obráceně.
// bez komentare :)
if ($user->gender === 'M') {
return $negativeSex ? '2' : '1';
}
if ($user->gender === 'F') {
return $negativeSex ? '1' : '2';
}
return '';
}
private function getUserIdByDRSId(string $drsId): ?int
{
$id = sqlQueryBuilder()
->select('id_user')
->from('drs_users')
->where(Operator::equals(['id_drs' => $drsId]))
->execute()->fetchOne();
if (!$id) {
return null;
}
return (int) $id;
}
private function prepareChildrenFromDatabaseUser(array $user): array
{
$data = array_filter($user['attributes'], fn ($x) => str_contains($x, 'Child'), ARRAY_FILTER_USE_KEY);
$children = [];
foreach ($this->getChildrenIndexMap() as $index => $value) {
$children[$index] = [
'ChildName' => $data["{$value}ChildName"] ?? '',
'ChildSex' => $data["{$value}ChildSex"] ?? '',
'ChildBirthDay' => $data["{$value}ChildBirthDay"] ?? '',
];
}
return array_filter($children, fn ($x) => !empty($x['ChildName']));
}
private function prepareChildrenFromExtendedValue(array $extendedValue): array
{
$extendedValue = array_filter($extendedValue, function ($x) {
return strpos($x['@attributes']['name'] ?? '', 'Child') !== false;
});
$children = [];
foreach ($extendedValue as $item) {
$item = $item['@attributes'] ?? [];
$childIndex = null;
$prefix = null;
foreach ($this->getChildrenIndexMap() as $index => $value) {
if (StringUtil::startsWith($item['name'] ?? '', $value)) {
$childIndex = $index;
$prefix = $value;
break;
}
}
if ($childIndex === null) {
continue;
}
$childKey = str_replace($prefix, '', $item['name']);
$children[$childIndex][$childKey] = $item['value'];
}
return array_filter($children, function ($x) { return !empty($x['ChildName']); });
}
private function getChildrenIndexMap(): array
{
return [
0 => 'First',
1 => 'Second',
2 => 'Third',
3 => 'Fourth',
];
}
private function getItemsAsIndexedArray(array $items): array
{
if (ArrayUtil::isDictionary($items)) {
$items = [$items];
}
return $items;
}
private function substr(string $string, int $maxLength = 50): string
{
return substr($string, 0, $maxLength);
}
private function getUserTimestamp(): ?array
{
if ($this->mode === self::MODE_FULL) {
return null;
}
$dbcfg = \Settings::getDefault();
$drs = $dbcfg->loadValue('drs');
return $drs[static::getType()]['timestamp'] ?? null;
}
// docasne logovani zmen zakazniku
private function debugLogUserChange(int $userId, string $method, array $data): void
{
$this->logger->log(
'[pompo] User change - ID: '.$userId.'; METHOD: '.$method,
array_merge($data, ['method' => $method])
);
}
/**
* Updatuje `user` timestampy v DB settingach.
*/
private function updateTimestamps(bool $sql = true, bool $local = true): void
{
if (!$this->timestampLocal && !$this->timestampSql) {
return;
}
$dbcfg = \Settings::getDefault();
$drs = $dbcfg->loadValue('drs') ?? [];
$timestampSQLValue = ($sql && $this->timestampSql) ? $this->timestampSql : ($drs[static::getType()]['timestamp']['sql'] ?? null);
$timestampLocalValue = ($local && $this->timestampLocal) ? $this->timestampLocal : ($drs[static::getType()]['timestamp']['local'] ?? null);
$drs[static::getType()]['timestamp'] = [
'sql' => $timestampSQLValue,
'local' => $timestampLocalValue,
];
$drs[static::getType()]['updated'] = time();
$dbcfg->saveValue('drs', $drs, false);
}
}