'processUserGroup', 'Zakaznici' => 'processUser', 'Adresy' => 'processUserAddress', 'CenoveUrovneVazby' => 'processUserGroupPriceListRelation', ]; } public function useTestConnection(): bool { return !$this->configuration->useProductionHelios(); } public static function getIdField(): ?string { return 'IdZakaznik'; } public function processToHelios(): void { if (isLocalDevelopment()) { return; } $lastSyncTime = $this->getLastSyncTime(); $orSpecs = ['zu.id_znz IS NULL']; if ($lastSyncTime) { $orSpecs[] = 'u.date_updated IS NOT NULL AND u.date_updated > :filterDate'; } $qb = sqlQueryBuilder() ->select('u.*, zu.id_znz, zu.id_znz_invoice, zu.id_znz_delivery') ->from('users', 'u') ->leftJoin('u', 'znz_users', 'zu', 'zu.id_user = u.id') ->andWhere( Operator::andX( $this->getUsersToHeliosSpec(), // podminky pro zapis uzivatele Operator::orX($orSpecs) ) ) // order by date_updated with newly registered at the beginning ->orderBySql('zu.id_znz IS NULL DESC, u.date_updated ASC') ->groupBy('u.id'); if ($lastSyncTime) { $qb->setParameter('filterDate', $lastSyncTime->format('Y-m-d H:i:s')); } // max 250 per one sync run $qb->setMaxResults(250); $updateLastSyncTime = null; foreach ($qb->execute() as $item) { $user = new \User(); $user->loadData($item); $userData = $this->getUserData($user, !empty($item['id_znz']) ? (int) $item['id_znz'] : null); // save date_updated of last synchronized item if (!empty($item['id_znz']) && !empty($item['date_updated'])) { $updateLastSyncTime = (new \DateTime($item['date_updated']))->getTimestamp(); } try { $znzId = ZNZUtil::withRetryStrategy(fn () => $this->getZNZApi()->updateUser($userData), false); } catch (\Exception $e) { $this->logger->log($e); continue; } if (empty($item['id_znz'])) { $this->znzUtil->createMapping(self::$type, $znzId, (int) $item['id']); } $addresses = $this->getUserAddresses( $user, !empty($item['id_znz_invoice']) ? (int) $item['id_znz_invoice'] : null, !empty($item['id_znz_delivery']) ? (int) $item['id_znz_delivery'] : null ); $invoiceAddress = $addresses['invoice']; $deliveryAddress = $addresses['delivery']; // pokud uz je fakturacni adresa v Heliosu zalozena, nebo je vyplnena adresa na e-shopu if ($item['id_znz_invoice'] || !empty($invoiceAddress['firstname'])) { try { $znzInvoiceId = ZNZUtil::withRetryStrategy(fn () => $this->getZNZApi()->updateUserAddress($invoiceAddress), false); } catch (\Exception $e) { $this->logger->log($e); } if (empty($item['id_znz_invoice']) && isset($znzInvoiceId)) { sqlQueryBuilder() ->update('znz_users') ->directValues(['id_znz_invoice' => $znzInvoiceId]) ->where(Operator::equals(['id_user' => $user->id])) ->execute(); } } // pokud uz je dorucovaci adresa v Heliosu zalozena, nebo je vyplnena adresa na e-shopu if ($item['id_znz_delivery'] || !empty($deliveryAddress['firstname'])) { try { $znzDeliveryId = ZNZUtil::withRetryStrategy(fn () => $this->getZNZApi()->updateUserAddress($deliveryAddress), false); } catch (\Exception $e) { $this->logger->log($e); } if (empty($item['id_znz_delivery']) && isset($znzDeliveryId)) { sqlQueryBuilder() ->update('znz_users') ->directValues(['id_znz_delivery' => $znzDeliveryId]) ->where(Operator::equals(['id_user' => $user->id])) ->execute(); } } } if ($updateLastSyncTime) { $this->updateLastSyncTime($updateLastSyncTime); } } public function getUserData(\User $user, ?int $znzId = null): array { $user->fetchAddresses(); $data = [ 'email' => $user->email, 'firstname' => $user->invoice['name'], 'middlename' => '', 'lastname' => $user->invoice['surname'], 'group_id' => $this->getUserGroupId($user), '_website' => $this->getUserWebsite($user), 'helios_customer_id' => $znzId, 'helios_activity_type' => null, 'helios_legal_form' => 2, // 0=Právnická osoba 1=Fyzická osoba 2=Soukromá osoba 3=Neurčeno ]; return $data; } protected function processUser(array $item): void { $userId = $this->znzUtil->getMapping(self::$type, $item['IdZakaznik']); if ($item['meta']['delete']) { if ($userId) { sqlQueryBuilder() ->delete('users') ->where(Operator::equals(['id' => $userId])) ->execute(); $this->logger->activity(sprintf('Proběhlo smazání uživatele: %s (%s)', $userId, $item['email'] ?? ''), [ 'item' => $item, ]); } return; } if (empty($item['email'])) { return; } if (!$userId) { $registrationData = [ 'email' => $item['email'], 'passw' => !empty($item['HashHeslo']) ? $item['HashHeslo'] : '', 'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'), ]; try { $userId = sqlGetConnection()->transactional(function () use ($item, $registrationData) { sqlQueryBuilder() ->insert('users') ->directValues($registrationData) ->execute(); $userId = (int) sqlInsertId(); $this->znzUtil->createMapping(self::$type, $item['IdZakaznik'], $userId); return $userId; }); } catch (UniqueConstraintViolationException $e) { if ($this->tryMergeWithNewsletterUser($userId, $registrationData)) { $this->znzUtil->createMapping(self::$type, $item['IdZakaznik'], $userId); } else { throw new ZNZException( sprintf('Unable to create user because of duplicated email: %s:%s', $item['IdZakaznik'], $item['email']), [ 'user' => $item, ] ); } } if ($this->configuration->isB2BShop()) { $this->activateUserB2BFeeds($userId); } } $hasEmptyPassword = sqlQueryBuilder() ->select('1') ->from('users') ->where(Operator::equals(['id' => $userId, 'passw' => ''])) ->execute()->fetchOne(); $update = [ 'id_language' => $this->getUserLanguageByWebsite($item['IdWebsite']), 'email' => $item['email'], ]; // pokud se nejedna o B2B shop, tak aktualizuju i figure zakaznika if (!$this->configuration->isB2BShop()) { $update['figure'] = $item['Aktivni'] ? 'Y' : 'N'; } if ($hasEmptyPassword && !empty($item['HashHeslo'])) { $update['passw'] = $item['HashHeslo']; } try { sqlQueryBuilder() ->update('users') ->directValues($update) ->where(Operator::equals(['id' => $userId])) ->execute(); } catch (UniqueConstraintViolationException $e) { if (!$this->tryMergeWithNewsletterUser($userId, $update)) { throw new ZNZException( sprintf('Unable to update user "%s" because of duplicated email: %s:%s', $userId, $item['IdZakaznik'], $item['email']), [ 'item' => $item, 'userId' => $userId, ] ); } } $this->updateUserGroup($userId, $item['IdZakaznickaSkupina']); $this->updateUserData($userId, $item); if ($this->configuration->isB2BShop()) { $this->activateB2BUser($userId, $item); } } protected function processUserAddress(array $item): void { if (!($mapping = $this->znzUtil->getUserMapping($item['IdZakaznik']))) { return; } if ($item['meta']['delete']) { return; } $prefix = ''; $mappingField = 'id_znz_invoice'; if (!$item['Fakturacni']) { $prefix = 'delivery_'; $mappingField = 'id_znz_delivery'; } $userName = $this->znzUtil->getUserNameParts($item['Nazev']); $update = [ $prefix.'name' => $userName['name'], $prefix.'surname' => $userName['surname'], $prefix.'firm' => $item['DruhyNazev'], $prefix.'city' => $item['Misto'], $prefix.'street' => $item['Ulice'], $prefix.'zip' => $item['PSC'], $prefix.'country' => $this->znzUtil->getCountryCodeForHelios($item['IdZeme'] ?? Contexts::get(CountryContext::class)->getDefaultId()), $prefix.'phone' => $item['Telefon'], ]; if ($item['Fakturacni']) { if (!empty($item['DIC'])) { $update[$prefix.'dic'] = trim($item['DIC']); } // fakturacni email if (!empty($item['Email'])) { $update['copy_email'] = $item['Email']; } } sqlQueryBuilder() ->update('users') ->directValues($update) ->where(Operator::equals(['id' => $mapping['id_user']])) ->execute(); if (empty($mapping['id_znz_invoice'])) { sqlQueryBuilder() ->update('znz_users') ->directValues( [ $mappingField => $item['IdAdresa'], ] ) ->where(Operator::equals(['id_user' => $mapping['id_user']])) ->execute(); } } protected function processUserGroup(array $item): void { if ($this->isDeleteMessage($item)) { if ($userGroupId = $this->znzUtil->getMapping(self::$typeGroups, $item['meta']['unique_id'])) { sqlQueryBuilder() ->delete('users_groups') ->where(Operator::equals(['id' => $userGroupId])) ->execute(); } return; } if (!($userGroupId = $this->znzUtil->getMapping(self::$typeGroups, $item['IdZakaznickaSkupina']))) { sqlGetConnection()->transactional(function () use ($item) { sqlQueryBuilder() ->insert('users_groups') ->directValues( [ 'name' => $item['Nazev'], ] )->execute(); $userGroupId = (int) sqlInsertId(); $this->znzUtil->createMapping(self::$typeGroups, $item['IdZakaznickaSkupina'], $userGroupId); return $userGroupId; }); } try { sqlQueryBuilder() ->update('users_groups') ->directValues( [ 'name' => $item['Nazev'], 'descr' => $item['Cislo'], ] ) ->where(Operator::equals(['id' => $userGroupId])) ->execute(); } catch (UniqueConstraintViolationException $e) { sqlQueryBuilder() ->update('users_groups') ->directValues( [ 'name' => "{$item['Nazev']} (2)", 'descr' => $item['Cislo'], ] ) ->where(Operator::equals(['id' => $userGroupId])) ->execute(); } } public function processUserGroupPriceListRelation(array $item): void { // pokud se nejedna o B2B rezim a poradi je vetsi jak 1, tak to ignoruju if (!$this->configuration->isB2BMode() && ($item['Poradi'] ?? null) > 1) { return; } if (!($userGroupId = $this->znzUtil->getMapping(self::$typeGroups, $item['IdZakaznickaSkupina']))) { throw new RabbitRetryMessageException('User group not found'); } if (!($priceListId = $this->znzUtil->getMapping(PriceListSynchronizer::getType(), $item['CenovaUroven']))) { throw new RabbitRetryMessageException('Price list not found'); } $update = ['id_pricelist' => $priceListId]; if ($this->configuration->isB2BShop()) { $data = $this->getUserGroupData($userGroupId); $data['znz']['stores'][$item['Poradi'] ?? 0] = null; if (!empty($item['IdSklad'])) { if ($storeId = $this->znzUtil->getMapping('store', $item['IdSklad'])) { $data['znz']['stores'][$item['Poradi'] ?? 0] = $storeId; } } $update['data'] = json_encode($data); } sqlQueryBuilder() ->update('users_groups') ->directValues($update) ->where(Operator::equals(['id' => $userGroupId])) ->execute(); } public function getUserAddresses(\User $user, ?int $znzInvoiceId = null, ?int $znzDeliveryId = null): array { // nacitam z masteru, protoze se stalo, ze se uzivatel registrovat v 14:40:58 a v 14:41:00 se spustila sync, ktera // ho zacala odesilat, ale fetchAddresses sel na slave, kde ten uzivatel jeste chybel a timpadem se udaje nenacetly QueryHint::withRouteToMaster(fn () => $user->fetchAddresses()); return [ 'invoice' => $this->getUserAddress($user, $znzInvoiceId), 'delivery' => $this->getUserAddress($user, $znzDeliveryId, 'delivery'), ]; } private function getUserGroupId(\User $user): int { $groupIds = $this->configuration->getSettings()['user']['groupId'] ?? []; $groupId = $groupIds[$user->id_language] ?? null; if (empty($groupId)) { $groupId = reset($groupIds); if (!empty($groupId)) { return (int) $groupId; } return 80; } return (int) $groupId; } private function getUserAddress(\User $user, ?int $znzId, string $type = 'invoice'): array { return [ 'helios_addr_id' => $znzId, 'firstname' => $user->{$type}['name'], 'middlename' => '', 'lastname' => $user->{$type}['surname'], 'company' => $user->{$type}['firm'], 'street' => $user->{$type}['street'], 'city' => $user->{$type}['city'], 'postcode' => $user->{$type}['zip'], 'telephone' => $user->{$type}['phone'], 'vat_id' => $user->invoice['dic'], 'helios_company_registration_number' => $user->invoice['ico'], 'region_id' => '', 'country_id' => $this->getUserCountry($user, $type), '_email' => $user->invoice['email'], '_website' => $this->getUserWebsite($user), '_address_default_billing_' => $type === 'invoice' ? 1 : 0, '_address_default_shipping_' => $type === 'delivery' ? 1 : 0, ]; } private function getUserWebsite(\User $user): string { return $this->znzUtil->getCurrentWebsite( $this->getUserLanguage($user) ); } private function getUserLanguage(\User $user): string { $language = $user->id_language ?: Contexts::get(LanguageContext::class)->getDefaultId(); if (empty($user->id_language) && !empty($user->invoice['country'])) { $language = match ($user->invoice['country']) { 'CZ' => 'cs', 'SK' => 'sk', 'IT' => 'it', 'FR' => 'fr', 'DE' => 'de', default => Contexts::get(LanguageContext::class)->getDefaultId(), }; } return $language; } private function getUserCountry(\User $user, string $type): string { $country = $user->{$type}['country']; if (empty($country)) { $country = match ($this->getUserLanguage($user)) { 'cs' => 'CZ', 'sk' => 'SK', 'it' => 'IT', 'fr' => 'FR', 'de' => 'DE', default => Contexts::get(CountryContext::class)->getDefaultId(), }; } return $this->znzUtil->getCountryCodeForHelios($country); } private function getUserLanguageByWebsite(string $website): string { $websites = $this->configuration->getSupportedWebsites(); $language = reset($websites)['language'] ?? Contexts::get(LanguageContext::class)->getDefaultId(); if ($websites[$website] ?? false) { $language = $websites[$website]['language'] ?? $language; } if (is_array($language)) { $language = reset($language); } return $language; } private function updateUserData(int $userId, array $item): void { $turnoverData = []; if (!empty($item['SlevoveMeze'])) { $turnoverData = [ 'slevaProcentem' => $item['slevaProcentem'], 'MenaObratu' => $item['MenaObratu'], 'Obrat' => $item['Obrat'], 'SledovatObratDnu' => $item['SledovatObratDnu'], 'SlevoveMeze' => json_decode($item['SlevoveMeze'] ?: '', true) ?: [], ]; } $data = [ 'feeds' => [ 'ObrazkyPovoleny' => $item['ObrazkyPovoleny'] ?? false, 'PopiskyPovoleny' => $item['PopiskyPovoleny'] ?? false, ], 'turnoverData' => $turnoverData, ]; $user = new \User(); $user->id = $userId; $user->setCustomData('znz', $data); } private function updateUserGroup(int $userId, $znzUserGroupId): void { $znzGroups = array_map(fn ($x) => $x['id_users_group'], sqlQueryBuilder() ->select('id_users_group') ->from('znz_users_groups') ->execute()->fetchAllAssociative()); if (!empty($znzGroups)) { sqlQueryBuilder() ->delete('users_groups_relations') ->andWhere(Operator::equals(['id_user' => $userId])) ->andWhere(Operator::inIntArray($znzGroups, 'id_group')) ->execute(); } $userGroupId = $this->znzUtil->getMapping(self::$typeGroups, $znzUserGroupId); // pokud mam ZNZ Group ID, ale nepodarilo se najit skupinu na shopu, tak zaloguju chybu do activity logu if (!empty($znzUserGroupId) && !$userGroupId) { $this->logger->activity( sprintf('Uživatele "%s" se nepodařilo zařadit do skupiny "%s", protože skupina neexistuje!', $userId, $znzUserGroupId), [ 'userId' => $userId, 'znzGroupId' => $znzUserGroupId, ] ); } if ($userGroupId) { $this->setUserGroup($userId, $userGroupId); } } private function setUserGroup(int $userId, int $userGroupId): void { try { sqlQueryBuilder() ->insert('users_groups_relations') ->directValues( [ 'id_user' => $userId, 'id_group' => $userGroupId, ] )->execute(); } catch (UniqueConstraintViolationException) { } } private function tryMergeWithNewsletterUser(?int &$userId, array $data): bool { $duplicatedUser = sqlQueryBuilder() ->select('id, figure, get_news, date_subscribe, date_unsubscribe') ->from('users') ->where(Operator::equals(['email' => $data['email']])) ->execute()->fetchAssociative(); if ($duplicatedUser['id'] === $userId) { return false; } $isNewsletterUser = false; // je to newsletter user if ($duplicatedUser['figure'] === 'N' && $duplicatedUser['get_news'] === 'Y') { $isNewsletterUser = true; } if (!$isNewsletterUser) { return false; } $data['get_news'] = $duplicatedUser['get_news']; $data['date_subscribe'] = $duplicatedUser['date_subscribe']; $data['date_unsubscribe'] = $duplicatedUser['date_unsubscribe']; $deleteDuplicatedUser = true; if ($userId === null && $duplicatedUser['id']) { $userId = $duplicatedUser['id']; $deleteDuplicatedUser = false; } sqlGetConnection()->transactional(function () use ($duplicatedUser, $userId, $deleteDuplicatedUser, $data) { if ($deleteDuplicatedUser) { sqlQueryBuilder() ->delete('users') ->where(Operator::equals(['id' => $duplicatedUser['id']])) ->execute(); } sqlQueryBuilder() ->update('users') ->directValues($data) ->where(Operator::equals(['id' => $userId])) ->execute(); }); return true; } private function getUserGroupData(int $userGroupId): array { $data = sqlQueryBuilder() ->select('data') ->from('users_groups') ->where(Operator::equals(['id' => $userGroupId])) ->execute()->fetchOne(); return json_decode($data ?: '', true) ?? []; } private function activateB2BUser(int $userId, array $item): void { // activate inactive user if they need to be assigned to a group if (!$this->isUserActive($userId) && !empty($item['IdZakaznickaSkupina'])) { sqlQueryBuilder() ->update('users') ->directValues(['figure' => 'Y']) ->where(Operator::equals(['id' => $userId])) ->execute(); $this->sendPasswordEmail($userId); $this->logger->activity( message: "Provedena aktivace B2B účtu `{$item['email']}` a odeslán mail pro nastavení hesla", data: ['userId' => $userId], severity: ActivityLog::SEVERITY_NOTICE ); } // activate B2B feeds $this->activateUserB2BFeeds($userId); // assign user to B2B group $this->setUserGroup($userId, $this->configuration->getB2BGroup()); } private function sendPasswordEmail(int $userId): void { if (!($user = \User::createFromId($userId))) { return; } $emailService = clone $this->passwordResetAdminEmail; $this->contextManager->activateContexts( [LanguageContext::class => $user->id_language ?: Contexts::get(LanguageContext::class)->getDefaultId()], function () use ($emailService, $user) { $message = $emailService->getEmail(); $message['to'] = $user->email; $emailService->sendEmail($message); } ); } private function activateUserB2BFeeds(int $userId): void { if (!findModule(\Modules::XML_FEEDS_B2B)) { return; } // prvotni aktivace feedu pro B2B uzivatele (vygenerovat token pokud jeste neexistuje) sqlQueryBuilder() ->update('users') ->directValues(['feed_token' => $this->tokenGenerator->generate(15)]) ->andWhere(Operator::equals(['id' => $userId])) ->andWhere('feed_token IS NULL') ->execute(); } private function isUserActive(int $userId): bool { return sqlQueryBuilder() ->select('figure') ->from('users') ->where(Operator::equals(['id' => $userId])) ->sendToMaster() ->execute()->fetchOne() === 'Y'; } private function getUsersToHeliosSpec(): callable { if ($this->configuration->isB2BShop()) { // v pripade B2B uzivatele neresime viditelnost uzivatele // protoze chceme zapsat i neaktivniho uzivatele, kterej vytvoril pozadavek na registraci return function (QueryBuilder $qb) { $qb->leftJoin('u', 'users_groups_relations', 'ugr', 'ugr.id_user = u.id'); return Operator::inIntArray( array_filter([$this->configuration->getB2BGroup(), $this->configuration->getB2BRegistrationGroup()]), 'ugr.id_group' ); }; } // v pripade B2C zapisujeme pouze viditelne uzivatele return Operator::equals(['u.figure' => 'Y']); } }