581 lines
19 KiB
PHP
581 lines
19 KiB
PHP
<?php
|
|
|
|
namespace KupShop\GraphQLBundle\ApiAdmin\Util;
|
|
|
|
use KupShop\CatalogBundle\Query\Search;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserBonusPointCollection;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserCollection;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\Parameters;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Enum\BonusPointStatus;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\AddressInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserAddressInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserBonusPointFilterInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserGroupsRelationInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserNewsletterInput;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserAddressMutateResponse;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserMutateResponse;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\User;
|
|
use KupShop\GraphQLBundle\ApiAdmin\Types\User\UserBonusPoint;
|
|
use KupShop\GraphQLBundle\ApiShared\ApiUtil;
|
|
use KupShop\GraphQLBundle\ApiShared\Util\ParametersAssembler;
|
|
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
|
|
use KupShop\GraphQLBundle\Exception\GraphQLValidationException;
|
|
use KupShop\KupShopBundle\Context\CountryContext;
|
|
use KupShop\KupShopBundle\Email\PasswordResetAdminEmail;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Database\QueryHint;
|
|
use KupShop\KupShopBundle\Util\Mail\EmailCheck;
|
|
use KupShop\KupShopBundle\Util\ObjectUtil;
|
|
use KupShop\PricelistBundle\Util\PriceListWorker;
|
|
use KupShop\UserBundle\Security\LegacyPasswordEncoder;
|
|
use KupShop\UserBundle\Util\UserConsent;
|
|
use Query\Operator;
|
|
use Query\QueryBuilder;
|
|
use Symfony\Contracts\Service\Attribute\Required;
|
|
|
|
class UserUtil
|
|
{
|
|
/**
|
|
* @var ParametersAssembler
|
|
*/
|
|
private $parametersAssembler;
|
|
|
|
#[Required]
|
|
public PasswordResetAdminEmail $passwordResetEmail;
|
|
|
|
public function __construct(
|
|
private readonly EmailCheck $emailCheck,
|
|
private readonly UserConsent $userConsent,
|
|
private readonly LegacyPasswordEncoder $passwordEncoder,
|
|
private readonly ?PriceListWorker $priceListWorker = null,
|
|
private readonly ?UserAddressesUtil $userAddressesUtil = null,
|
|
) {
|
|
}
|
|
|
|
public function getUserCollection(int $offset, int $limit, ?Parameters $userSort, ?Parameters $userFilter): UserCollection
|
|
{
|
|
$filterSpec = $this->createUserCollectionFilter($offset, $limit, $userSort, $userFilter);
|
|
|
|
$users = $this->getUsers($filterSpec, $totalCount);
|
|
|
|
return (new UserCollection($users))
|
|
->setItemsTotalCount($totalCount)
|
|
->setLimit($limit)
|
|
->setOffset($offset);
|
|
}
|
|
|
|
public function getUser(?int $id, ?string $email, ?string $cartId = null): ?User
|
|
{
|
|
if (!$id && !$email && !$cartId) {
|
|
throw new GraphQLValidationException('Cannot query `user` without id or email or cartId provided!');
|
|
}
|
|
|
|
$users = $this->getUsers(Operator::equals(array_filter(['u.id' => $id, 'u.email' => $email, 'u.user_key' => $cartId]), 'OR'));
|
|
|
|
if (!($user = reset($users))) {
|
|
throw new GraphQLNotFoundException('User not found!');
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @return User[]
|
|
*/
|
|
public function getUsers(callable $filterSpec, ?int &$totalCount = null): array
|
|
{
|
|
$useTotalCount = count(func_get_args()) > 1;
|
|
|
|
$qb = sqlQueryBuilder()
|
|
->addCalcRows()
|
|
->select('u.*')
|
|
->from('users', 'u')
|
|
->andWhere($filterSpec)
|
|
->groupBy('u.id');
|
|
|
|
$users = [];
|
|
foreach ($qb->execute() as $item) {
|
|
$user = new \User();
|
|
$user->loadData($item);
|
|
$user->fetchAddresses($item);
|
|
|
|
$users[$item['id']] = $user;
|
|
}
|
|
|
|
if ($useTotalCount) {
|
|
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
|
|
}
|
|
|
|
$this->processUsersMultiFetches($users);
|
|
|
|
return ApiUtil::wrapItems($users, User::class);
|
|
}
|
|
|
|
public function getUsersBonusPoints(int $offset, int $limit, ?Parameters $sort, ?UserBonusPointFilterInput $filter): UserBonusPointCollection
|
|
{
|
|
$qb = sqlQueryBuilder()
|
|
->addCalcRows()
|
|
->select('bp.*')
|
|
->from('bonus_points', 'bp')
|
|
->andWhere(ApiUtil::getLimitSpec($offset, $limit))
|
|
->andWhere(ApiUtil::getSortSpec($sort, 'bp'));
|
|
|
|
if ($filter?->id) {
|
|
$qb->andWhere(Operator::inIntArray($filter->id, 'bp.id'));
|
|
}
|
|
|
|
if ($filter?->userId) {
|
|
$qb->andWhere(Operator::inIntArray($filter->userId, 'bp.id_user'));
|
|
}
|
|
|
|
if ($filter?->orderId) {
|
|
$qb->andWhere(Operator::inIntArray($filter->orderId, 'bp.id_order'));
|
|
}
|
|
|
|
if ($filter?->status) {
|
|
$qb->andWhere(Operator::inStringArray(array_map(fn (BonusPointStatus $status) => $status->value, $filter->status), 'bp.status'));
|
|
}
|
|
|
|
if ($filter?->dateCreated) {
|
|
$qb->andWhere(ApiUtil::getDateTimeFilter($filter->dateCreated, 'bp.date_created'));
|
|
}
|
|
|
|
$data = $qb->execute();
|
|
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
|
|
|
|
return (new UserBonusPointCollection(
|
|
ApiUtil::wrapItems($data, UserBonusPoint::class))
|
|
)
|
|
->setOffset($offset)
|
|
->setLimit($limit)
|
|
->setItemsTotalCount($totalCount);
|
|
}
|
|
|
|
public function addOrRemoveUserGroupsRelations(UserGroupsRelationInput $input): UserMutateResponse
|
|
{
|
|
// zkontroluju, ze uzivatel existuje
|
|
$this->validateUser($input->userId);
|
|
|
|
// zkontroluju, ze vsechny uzivatelske skupiny existuji
|
|
foreach ($input->userGroupIds as $group) {
|
|
$this->validateUserGroup($group);
|
|
}
|
|
|
|
if ($input->overwrite) {
|
|
sqlQueryBuilder()
|
|
->delete('users_groups_relations')
|
|
->where(Operator::equals(['id_user' => $input->userId]))
|
|
->execute();
|
|
}
|
|
|
|
foreach ($input->userGroupIds as $group) {
|
|
sqlQueryBuilder()
|
|
->insert('users_groups_relations')
|
|
->directValues([
|
|
'id_user' => $input->userId,
|
|
'id_group' => $group,
|
|
])
|
|
->onDuplicateKeyUpdate(['id_group' => $group, 'id_user' => $input->userId])
|
|
->execute();
|
|
}
|
|
|
|
return new UserMutateResponse(true, $this->getUpdatedUser($input->userId));
|
|
}
|
|
|
|
public function createOrUpdateUser(UserInput $input): UserMutateResponse
|
|
{
|
|
if (empty($input->id) && !ObjectUtil::isPropertyInitialized($input, 'email')) {
|
|
throw new GraphQLValidationException('Field "email" is required when creating user!');
|
|
}
|
|
|
|
// zkontroloval, ze uzivatel se zadanym ID existuje
|
|
if (isset($input->id)) {
|
|
$userExists = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users')
|
|
->where(Operator::equals(['id' => $input->id]))
|
|
->execute()->fetchOne();
|
|
|
|
if (!$userExists) {
|
|
throw new GraphQLNotFoundException(sprintf('User with ID "%s" does not exists!', $input->id));
|
|
}
|
|
}
|
|
|
|
// zkontroluju mail
|
|
if (ObjectUtil::isPropertyInitialized($input, 'email')) {
|
|
$this->validateUserEmail($input->email, !empty($input->id) ? $input->id : null);
|
|
}
|
|
|
|
// zkontroluju, ze zadavam existujici cenik
|
|
if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
|
|
$this->validateUserPriceList($input->priceListId);
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'password')) {
|
|
$this->validateUserPassword($input->password);
|
|
}
|
|
|
|
$userData = $this->getUserData($input);
|
|
|
|
$isNewUser = false;
|
|
$userId = !empty($input->id) ? $input->id : null;
|
|
// vytvoreni zakaznika
|
|
if (!$userId) {
|
|
$defaults = [
|
|
'figure' => 'Y',
|
|
'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'),
|
|
];
|
|
|
|
$isNewUser = true;
|
|
$insert = array_merge($userData, $defaults);
|
|
|
|
$userId = sqlGetConnection()->transactional(function () use ($insert) {
|
|
sqlQueryBuilder()
|
|
->insert('users')
|
|
->directValues($insert)
|
|
->execute();
|
|
|
|
return (int) sqlInsertId();
|
|
});
|
|
} elseif (!empty($userData)) {
|
|
// aktualizace zakaznika
|
|
sqlQueryBuilder()
|
|
->update('users')
|
|
->directValues($userData)
|
|
->where(Operator::equals(['id' => $userId]))
|
|
->execute();
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'data')) {
|
|
ApiUtil::updateObjectCustomData('users', $userId, $input->data, 'custom_data');
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'newsletter')) {
|
|
$this->updateUserNewsletter($userId, $isNewUser, $input->newsletter);
|
|
}
|
|
|
|
if ($input->sendPasswordEmail) {
|
|
$this->sendPasswordResetEmail($userId);
|
|
}
|
|
|
|
return new UserMutateResponse(true, $this->getUpdatedUser($userId));
|
|
}
|
|
|
|
protected function getUserData(UserInput $input): array
|
|
{
|
|
$data = [];
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'email')) {
|
|
$data['email'] = $input->email;
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'password')) {
|
|
$data['passw'] = $input->password ? $this->passwordEncoder->hash($input->password) : '';
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'ico')) {
|
|
$data['ico'] = $input->ico;
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'dic')) {
|
|
$data['dic'] = $input->dic;
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'isActive')) {
|
|
$data['figure'] = $input->isActive ? 'Y' : 'N';
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
|
|
$data['id_pricelist'] = $input->priceListId;
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'invoiceAddress')) {
|
|
$address = $this->getUserAddress($input->invoiceAddress);
|
|
if (!empty($address)) {
|
|
$data = array_merge($data, $address);
|
|
}
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($input, 'deliveryAddress')) {
|
|
$address = $this->getUserAddress($input->deliveryAddress);
|
|
foreach ($address as $key => $value) {
|
|
if (in_array($key, ['ico', 'dic'])) {
|
|
continue;
|
|
}
|
|
|
|
$data['delivery_'.$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function updateUserNewsletter(int $userId, bool $isNewUser, ?UserNewsletterInput $newsletterInput): void
|
|
{
|
|
if (!$newsletterInput) {
|
|
return;
|
|
}
|
|
|
|
if (ObjectUtil::isPropertyInitialized($newsletterInput, 'isSubscribed') && $newsletterInput->isSubscribed !== null) {
|
|
$this->userConsent->updateNewsletter(
|
|
userId: $userId,
|
|
newsletter: $newsletterInput->isSubscribed ? 'Y' : 'N',
|
|
new_user: $isNewUser,
|
|
confirmed: true
|
|
);
|
|
}
|
|
}
|
|
|
|
public function getUpdatedUser(int $userId): User
|
|
{
|
|
$users = QueryHint::withRouteToMaster(fn () => $this->getUsers(Operator::equals(['u.id' => $userId])));
|
|
|
|
return reset($users);
|
|
}
|
|
|
|
protected function getUserAddress(AddressInput|UserAddressInput $input): array
|
|
{
|
|
// validovat zemi
|
|
if (ObjectUtil::isPropertyInitialized($input, 'country')) {
|
|
$countries = Contexts::get(CountryContext::class)->getAll();
|
|
if (empty($countries[$input->country])) {
|
|
throw new GraphQLValidationException(sprintf('Country "%s" does not exist!', $input->country));
|
|
}
|
|
}
|
|
|
|
return (array) $input;
|
|
}
|
|
|
|
protected function prepareUserDeliveryAddress(array $address): array
|
|
{
|
|
$userDeliveryAddress = [];
|
|
if (!$this->checkUser($address['userId']) ?? false) {
|
|
throw new GraphQLValidationException(sprintf('User with ID "%s" does not exist!', $address['userId']));
|
|
}
|
|
|
|
$address = $this->renameAddressFields($address);
|
|
|
|
foreach ($address as $key => $value) {
|
|
if (in_array($key, ['id', 'id_user'])) {
|
|
$userDeliveryAddress[$key] = $value;
|
|
continue;
|
|
}
|
|
|
|
$userDeliveryAddress['delivery_'.$key] = $value;
|
|
}
|
|
|
|
return $userDeliveryAddress;
|
|
}
|
|
|
|
private function renameAddressFields(array $address)
|
|
{
|
|
$address['id_user'] = $address['userId'];
|
|
$address['custom_address'] = $address['customAddress'] ?? '';
|
|
|
|
unset($address['userId']);
|
|
unset($address['customAddress']);
|
|
|
|
return $address;
|
|
}
|
|
|
|
private function checkUser(int $userId): ?int
|
|
{
|
|
return sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users')
|
|
->where(Operator::equals(['id' => $userId]))
|
|
->execute()->fetchOne() ?: null;
|
|
}
|
|
|
|
private function checkCurrency(string $currency): ?string
|
|
{
|
|
return sqlQueryBuilder()
|
|
->select('id')
|
|
->from('currencies')
|
|
->where(Operator::equals(['id' => $currency]))
|
|
->execute()->fetchOne() ?: null;
|
|
}
|
|
|
|
protected function validateUserPassword(?string $password): void
|
|
{
|
|
if ($password === null) {
|
|
return;
|
|
}
|
|
|
|
if (strlen($password) < 6) {
|
|
throw new GraphQLValidationException(
|
|
message: 'Password must be at least 6 characters long.',
|
|
extensions: ['field' => 'password'],
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function validateUserEmail(?string $email, ?int $userId = null): void
|
|
{
|
|
if (empty($email)) {
|
|
throw new GraphQLValidationException('Field "email" cannot be empty!');
|
|
}
|
|
|
|
$spec = [
|
|
Operator::equals(['email' => $email]),
|
|
];
|
|
|
|
if ($userId) {
|
|
$spec[] = Operator::not(
|
|
Operator::equals(['id' => $userId])
|
|
);
|
|
}
|
|
|
|
// kontrola, zda se nesnazim nastavit email, ktery uz ma jiny zakaznik
|
|
$emailExists = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users')
|
|
->where(Operator::andX($spec))
|
|
->execute()->fetchOne();
|
|
|
|
if ($emailExists) {
|
|
throw new GraphQLValidationException(
|
|
sprintf('Email "%s" is already set up for another user!', $email)
|
|
);
|
|
}
|
|
|
|
if (!$this->emailCheck->isEmailDomainValid($email)) {
|
|
throw new GraphQLValidationException(sprintf('Email "%s" is not valid!', $email));
|
|
}
|
|
}
|
|
|
|
protected function validateUser(int $userId): void
|
|
{
|
|
$userExists = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users')
|
|
->where(Operator::equals(['id' => $userId]))
|
|
->sendToMaster()->execute()->fetchOne();
|
|
|
|
if (!$userExists) {
|
|
throw new GraphQLNotFoundException(sprintf(
|
|
'User with ID "%s" does not exist!', $userId)
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function validateUserGroup(int $userGroup): void
|
|
{
|
|
$userGroupExists = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users_groups')
|
|
->where(Operator::equals(['id' => $userGroup]))
|
|
->execute()->fetchOne();
|
|
|
|
if (!$userGroupExists) {
|
|
throw new GraphQLValidationException(
|
|
sprintf('User group with ID "%s" does not exist!', $userGroup)
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function validateUserPriceList(?int $priceListId): void
|
|
{
|
|
if (!$this->priceListWorker) {
|
|
return;
|
|
}
|
|
|
|
if ($priceListId !== null && !$this->priceListWorker->getPriceList($priceListId)) {
|
|
throw new GraphQLValidationException(
|
|
sprintf('Price list with ID "%s" does not exists!', $priceListId)
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function createUserCollectionFilter(int $offset, int $limit, ?Parameters $sort, ?Parameters $filter): callable
|
|
{
|
|
$phone = $filter?->get('phone');
|
|
if (!empty($phone)) {
|
|
$filter->set('phone', Operator::like(['u.phone' => '%'.$phone.'%']));
|
|
} else {
|
|
$filter?->remove('phone');
|
|
}
|
|
|
|
if (!empty($filter?->get('name'))) {
|
|
$filter->set('name', Search::searchFields($filter->get('name'), [
|
|
['field' => 'u.name', 'match' => 'both'],
|
|
['field' => 'u.surname', 'match' => 'both'],
|
|
]));
|
|
} else {
|
|
$filter?->remove('name');
|
|
}
|
|
|
|
return function (QueryBuilder $qb) use ($offset, $limit, $sort, $filter) {
|
|
$this->parametersAssembler->assemblyInput($filter, $sort, $offset, $limit, $qb);
|
|
};
|
|
}
|
|
|
|
protected function saveDeliveryAddress(array $address): int
|
|
{
|
|
return sqlGetConnection()->transactional(function () use ($address) {
|
|
$qb = sqlQueryBuilder();
|
|
if ($id = ($address['id'] ?? false)) {
|
|
unset($address['id']);
|
|
$qb->update('users_addresses')
|
|
->directValues($address)
|
|
->where(Operator::equals(['id' => $id]))
|
|
->execute();
|
|
|
|
return $id;
|
|
}
|
|
|
|
$qb->insert('users_addresses')
|
|
->directValues($address)
|
|
->execute();
|
|
|
|
return sqlInsertId();
|
|
});
|
|
}
|
|
|
|
public function createOrUpdateUserAddress(UserAddressInput $input): UserAddressMutateResponse
|
|
{
|
|
$address = $this->getUserAddress($input);
|
|
|
|
if (isset($address['id'])) {
|
|
$addressExists = sqlQueryBuilder()
|
|
->select('id')
|
|
->from('users_addresses')
|
|
->where(Operator::equals(['id' => $address['id']]))
|
|
->execute()->fetchOne();
|
|
|
|
if (!$addressExists) {
|
|
throw new GraphQLNotFoundException(sprintf('Address with ID "%s" does not exists!', $address['id']));
|
|
}
|
|
}
|
|
|
|
$address = $this->prepareUserDeliveryAddress($address);
|
|
|
|
return new UserAddressMutateResponse(true, $this->saveDeliveryAddress($address));
|
|
}
|
|
|
|
protected function processUsersMultiFetches(array &$users): void
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @required
|
|
*/
|
|
public function setParametersAssembler(ParametersAssembler $parametersAssembler): void
|
|
{
|
|
$this->parametersAssembler = $parametersAssembler;
|
|
}
|
|
|
|
public function sendPasswordResetEmail(int $userId): void
|
|
{
|
|
$user = QueryHint::withRouteToMaster(fn () => \User::createFromId($userId));
|
|
$this->passwordResetEmail->setUser($user);
|
|
|
|
$email = $this->passwordResetEmail->getEmail();
|
|
|
|
$email['to'] = $user->email;
|
|
|
|
$this->passwordResetEmail->sendEmail($email);
|
|
}
|
|
}
|