diff --git a/src/Model/Customer/User/CustomerUserUpdateDataFactory.php b/src/Model/Customer/User/CustomerUserUpdateDataFactory.php index f91970f9bb..c8048ae415 100644 --- a/src/Model/Customer/User/CustomerUserUpdateDataFactory.php +++ b/src/Model/Customer/User/CustomerUserUpdateDataFactory.php @@ -9,17 +9,17 @@ use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserDataFactory as FrameworkCustomerUserDataFactory; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateData; -use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactoryInterface; +use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactory as FrameworkCustomerUserUpdateDataFactory; class CustomerUserUpdateDataFactory { /** - * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactoryInterface $customerUserUpdateDataFactory + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactory $customerUserUpdateDataFactory * @param \Shopsys\FrameworkBundle\Model\Customer\BillingAddressDataFactory $billingAddressDataFactory * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserDataFactory $customerUserDataFactory */ public function __construct( - protected readonly CustomerUserUpdateDataFactoryInterface $customerUserUpdateDataFactory, + protected readonly FrameworkCustomerUserUpdateDataFactory $customerUserUpdateDataFactory, protected readonly BillingAddressDataFactory $billingAddressDataFactory, protected readonly FrameworkCustomerUserDataFactory $customerUserDataFactory, ) { diff --git a/src/Model/Customer/User/RegistrationFacade.php b/src/Model/Customer/User/RegistrationFacade.php index 45403438cb..6efc565b77 100644 --- a/src/Model/Customer/User/RegistrationFacade.php +++ b/src/Model/Customer/User/RegistrationFacade.php @@ -4,12 +4,18 @@ namespace Shopsys\FrontendApiBundle\Model\Customer\User; +use DateTime; +use Doctrine\ORM\EntityManagerInterface; use Shopsys\FrameworkBundle\Component\Domain\Domain; use Shopsys\FrameworkBundle\Model\Customer\Exception\DuplicateEmailException; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserFacade; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateData; +use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactory as FrameworkCustomerUserUpdateDataFactory; use Shopsys\FrameworkBundle\Model\Newsletter\NewsletterFacade; +use Shopsys\FrameworkBundle\Model\Order\Exception\OrderNotFoundException; +use Shopsys\FrameworkBundle\Model\Order\OrderFacade; +use Shopsys\FrontendApiBundle\Model\Order\Exception\RegisterByOrderIsNotPossibleUserError; class RegistrationFacade { @@ -18,12 +24,18 @@ class RegistrationFacade * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserFacade $customerUserFacade * @param \Shopsys\FrameworkBundle\Model\Newsletter\NewsletterFacade $newsletterFacade * @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain + * @param \Shopsys\FrameworkBundle\Model\Order\OrderFacade $orderFacade + * @param \Doctrine\ORM\EntityManagerInterface $em + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUserUpdateDataFactory $frameworkCustomerUserUpdateDataFactory */ public function __construct( protected readonly CustomerUserUpdateDataFactory $customerUserUpdateDataFactory, protected readonly CustomerUserFacade $customerUserFacade, protected readonly NewsletterFacade $newsletterFacade, protected readonly Domain $domain, + protected readonly OrderFacade $orderFacade, + protected readonly EntityManagerInterface $em, + protected readonly FrameworkCustomerUserUpdateDataFactory $frameworkCustomerUserUpdateDataFactory, ) { } @@ -49,12 +61,36 @@ public function register(RegistrationData $registrationData): CustomerUser $customerUserUpdateData = $this->customerUserUpdateDataFactory->createFromRegistrationData($registrationData); - $customerUser = $this->customerUserFacade->create($customerUserUpdateData); + return $this->customerUserFacade->create($customerUserUpdateData); + } - if ($customerUser->isNewsletterSubscription()) { - $this->newsletterFacade->addSubscribedEmailIfNotExists($customerUser->getEmail(), $customerUser->getDomainId()); + /** + * @param string $orderUrlHash + * @param string $password + * @return \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser + */ + public function registerByOrder(string $orderUrlHash, string $password): CustomerUser + { + try { + $order = $this->orderFacade->getByUrlHashAndDomain($orderUrlHash, $this->domain->getId()); + } catch (OrderNotFoundException) { + throw new RegisterByOrderIsNotPossibleUserError('Order not found.'); } + if ($order->getCustomerUser() !== null) { + throw new RegisterByOrderIsNotPossibleUserError('Order is owned by another customer.'); + } + + if ($order->getCreatedAt() < new DateTime('-1 hour')) { + throw new RegisterByOrderIsNotPossibleUserError('Registration for a established order is possible only within an hour of establishment of an order.'); + } + + $customerUserUpdateData = $this->frameworkCustomerUserUpdateDataFactory->createFromOrder($order, $password); + $customerUser = $this->customerUserFacade->create($customerUserUpdateData); + + $order->setCustomerUser($customerUser); + $this->em->flush(); + return $customerUser; } diff --git a/src/Model/Mutation/Customer/User/CustomerUserMutation.php b/src/Model/Mutation/Customer/User/CustomerUserMutation.php index e6f7dc3328..0527f83044 100644 --- a/src/Model/Mutation/Customer/User/CustomerUserMutation.php +++ b/src/Model/Mutation/Customer/User/CustomerUserMutation.php @@ -155,26 +155,22 @@ public function registerMutation(Argument $argument, InputValidator $validator): $this->mergeCartFacade->mergeCartByUuidToCustomerCart($argument['input']['cartUuid'], $customerUser); } - if ($argument['input']['lastOrderUuid'] !== null) { - $this->orderFacade->pairCustomerUserWithOrderByOrderUuid($customerUser, $argument['input']['lastOrderUuid']); - } - $this->productListFacade->mergeProductListsToCustomerUser($argument['input']['productListsUuids'], $customerUser); - $deviceId = Uuid::uuid4()->toString(); + return $this->loginRegisteredCustomerUser($customerUser); + } - $this->customerUserLoginTypeFacade->updateCustomerUserLoginTypes( - $this->customerUserLoginTypeDataFactory->create($customerUser, LoginTypeEnum::WEB), - ); + /** + * @param \Overblog\GraphQLBundle\Definition\Argument $argument + * @return \Shopsys\FrontendApiBundle\Model\Security\LoginResultData + */ + public function registerByOrderMutation(Argument $argument): LoginResultData + { + $input = $argument['input']; + $customerUser = $this->registrationFacade->registerByOrder($input['orderUrlHash'], $input['password']); + $this->productListFacade->mergeProductListsToCustomerUser($input['productListsUuids'], $customerUser); - return $this->loginResultDataFactory->create( - $this->tokensDataFactory->create( - $this->tokenFacade->createAccessTokenAsString($customerUser, $deviceId), - $this->tokenFacade->createRefreshTokenAsString($customerUser, $deviceId), - ), - $this->mergeCartFacade->shouldShowCartMergeInfo(), - true, - ); + return $this->loginRegisteredCustomerUser($customerUser); } /** @@ -295,4 +291,26 @@ protected function checkCustomerUserCanBeDeleted( ); } } + + /** + * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser + * @return \Shopsys\FrontendApiBundle\Model\Security\LoginResultData + */ + protected function loginRegisteredCustomerUser(CustomerUser $customerUser): LoginResultData + { + $deviceId = Uuid::uuid4()->toString(); + + $this->customerUserLoginTypeFacade->updateCustomerUserLoginTypes( + $this->customerUserLoginTypeDataFactory->create($customerUser, LoginTypeEnum::WEB), + ); + + return $this->loginResultDataFactory->create( + $this->tokensDataFactory->create( + $this->tokenFacade->createAccessTokenAsString($customerUser, $deviceId), + $this->tokenFacade->createRefreshTokenAsString($customerUser, $deviceId), + ), + $this->mergeCartFacade->shouldShowCartMergeInfo(), + true, + ); + } } diff --git a/src/Model/Order/Exception/OrderCannotBePairedException.php b/src/Model/Order/Exception/RegisterByOrderIsNotPossibleUserError.php similarity index 68% rename from src/Model/Order/Exception/OrderCannotBePairedException.php rename to src/Model/Order/Exception/RegisterByOrderIsNotPossibleUserError.php index e69880ba31..e672ac5c15 100644 --- a/src/Model/Order/Exception/OrderCannotBePairedException.php +++ b/src/Model/Order/Exception/RegisterByOrderIsNotPossibleUserError.php @@ -8,9 +8,9 @@ use Shopsys\FrameworkBundle\Model\Order\Exception\OrderException; use Shopsys\FrontendApiBundle\Model\Error\UserErrorWithCodeInterface; -class OrderCannotBePairedException extends UserError implements OrderException, UserErrorWithCodeInterface +class RegisterByOrderIsNotPossibleUserError extends UserError implements OrderException, UserErrorWithCodeInterface { - protected const CODE = 'order-cannot-be-paired-with-new-registration'; + protected const string CODE = 'register-by-order-is-not-possible'; /** * {@inheritdoc} diff --git a/src/Model/Order/OrderApiFacade.php b/src/Model/Order/OrderApiFacade.php index ccd5f83264..becd91487c 100644 --- a/src/Model/Order/OrderApiFacade.php +++ b/src/Model/Order/OrderApiFacade.php @@ -4,28 +4,22 @@ namespace Shopsys\FrontendApiBundle\Model\Order; -use Doctrine\ORM\EntityManagerInterface; use Shopsys\FrameworkBundle\Model\Customer\Customer; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser; use Shopsys\FrameworkBundle\Model\Order\Exception\OrderNotFoundException; use Shopsys\FrameworkBundle\Model\Order\Order; use Shopsys\FrameworkBundle\Model\Order\OrderFacade; -use Shopsys\FrontendApiBundle\Model\Order\Exception\OrderCannotBePairedException; use Shopsys\FrontendApiBundle\Model\Resolver\Order\Exception\OrderNotFoundUserError; class OrderApiFacade { - protected const int ONE_HOUR_REGISTRATION_WINDOW = 3600; - /** * @param \Shopsys\FrontendApiBundle\Model\Order\OrderRepository $orderRepository * @param \Shopsys\FrameworkBundle\Model\Order\OrderFacade $orderFacade - * @param \Doctrine\ORM\EntityManagerInterface $em */ public function __construct( protected readonly OrderRepository $orderRepository, protected readonly OrderFacade $orderFacade, - protected readonly EntityManagerInterface $em, ) { } @@ -103,30 +97,6 @@ public function findLastOrderByCustomerUser(CustomerUser $customerUser): ?Order return $orderList[0]; } - /** - * @param \Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser $customerUser - * @param string $orderUuid - */ - public function pairCustomerUserWithOrderByOrderUuid(CustomerUser $customerUser, string $orderUuid): void - { - $order = $this->getByUuid($orderUuid); - - if ($order->getCustomerUser() !== null) { - throw new OrderCannotBePairedException('Order is owned by another customer.'); - } - - if ($order->getEmail() !== $customerUser->getEmail()) { - throw new OrderCannotBePairedException('Emails used in order and registration do not match.'); - } - - if ($order->getCreatedAt()->getTimestamp() < (time() - self::ONE_HOUR_REGISTRATION_WINDOW)) { - throw new OrderCannotBePairedException('Registration for a established order is possible only within an hour of establishment of an order.'); - } - - $order->setCustomerUser($customerUser); - $this->em->flush(); - } - /** * @param \Shopsys\FrameworkBundle\Model\Customer\Customer $customer * @param int $limit diff --git a/src/Model/Resolver/Customer/User/CustomerUserResolverMap.php b/src/Model/Resolver/Customer/User/CustomerUserResolverMap.php index d0a8f242bf..dfeb92e125 100644 --- a/src/Model/Resolver/Customer/User/CustomerUserResolverMap.php +++ b/src/Model/Resolver/Customer/User/CustomerUserResolverMap.php @@ -7,6 +7,7 @@ use Overblog\GraphQLBundle\Resolver\ResolverMap; use Shopsys\FrameworkBundle\Model\Customer\User\CustomerUser; use Shopsys\FrameworkBundle\Model\Customer\User\Role\CustomerUserRoleResolver; +use Shopsys\FrameworkBundle\Model\Newsletter\NewsletterFacade; use Shopsys\FrontendApiBundle\Model\Customer\User\LoginInfoFactory; use Shopsys\FrontendApiBundle\Model\Customer\User\LoginType\CustomerUserLoginTypeFacade; use Shopsys\FrontendApiBundle\Model\Customer\User\LoginType\Exception\MissingCustomerUserLoginTypeException; @@ -17,11 +18,13 @@ class CustomerUserResolverMap extends ResolverMap * @param \Shopsys\FrontendApiBundle\Model\Customer\User\LoginType\CustomerUserLoginTypeFacade $customerUserLoginTypeFacade * @param \Shopsys\FrontendApiBundle\Model\Customer\User\LoginInfoFactory $loginInfoFactory * @param \Shopsys\FrameworkBundle\Model\Customer\User\Role\CustomerUserRoleResolver $customerUserRoleResolver + * @param \Shopsys\FrameworkBundle\Model\Newsletter\NewsletterFacade $newsletterFacade */ public function __construct( protected readonly CustomerUserLoginTypeFacade $customerUserLoginTypeFacade, protected readonly LoginInfoFactory $loginInfoFactory, protected readonly CustomerUserRoleResolver $customerUserRoleResolver, + protected readonly NewsletterFacade $newsletterFacade, ) { } @@ -67,6 +70,9 @@ protected function map(): array 'roles' => function (CustomerUser $customerUser) { return $this->customerUserRoleResolver->getRolesForCustomerUser($customerUser); }, + 'newsletterSubscription' => function (CustomerUser $customerUser) { + return $this->newsletterFacade->isSubscribed($customerUser); + }, ]; return [ diff --git a/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/NameInputObjectDecorator.types.yaml b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/NameInputObjectDecorator.types.yaml new file mode 100644 index 0000000000..c1a309156e --- /dev/null +++ b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/NameInputObjectDecorator.types.yaml @@ -0,0 +1,23 @@ +NameInputObjectDecorator: + type: input-object + decorator: true + config: + fields: + firstName: + type: "String!" + description: "Customer user first name" + validation: + - NotBlank: + message: "Please enter first name" + - Length: + max: 100 + maxMessage: "First name cannot be longer than {{ limit }} characters" + lastName: + type: "String!" + description: "Customer user last name" + validation: + - NotBlank: + message: "Please enter last name" + - Length: + max: 100 + maxMessage: "Last name cannot be longer than {{ limit }} characters" diff --git a/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationByOrderInputDecorator.types.yaml b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationByOrderInputDecorator.types.yaml new file mode 100644 index 0000000000..e8e56a16d4 --- /dev/null +++ b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationByOrderInputDecorator.types.yaml @@ -0,0 +1,24 @@ +RegistrationByOrderInputDecorator: + type: input-object + decorator: true + config: + fields: + orderUrlHash: + type: "String!" + description: "Order URL hash" + validation: + - NotBlank: + message: "Please enter order URL hash" + password: + type: "Password!" + description: "Customer user password" + validation: + - NotBlank: + message: "Please enter new password" + - Length: + min: 6 + minMessage: "Password must be at least {{ limit }} characters long" + productListsUuids: + type: "[Uuid!]!" + description: "Uuids of product lists that should be merged to the product lists of the user after registration" + defaultValue: [] diff --git a/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationDataInputDecorator.types.yaml b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationDataInputDecorator.types.yaml index 464f1cea79..b7e9a1df17 100644 --- a/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationDataInputDecorator.types.yaml +++ b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/RegistrationDataInputDecorator.types.yaml @@ -1,30 +1,17 @@ RegistrationDataInputDecorator: type: input-object decorator: true + inherits: + - 'NameInputObjectDecorator' + - 'TelephoneInputObjectDecorator' + - 'BillingAddressInputObjectDecorator' + - 'CompanyInputObjectDecorator' config: description: "Represents the main input object to register customer user" fields: - firstName: - type: "String!" - description: "Customer user first name" - validation: - - NotBlank: - message: "Please enter first name" - - Length: - max: 100 - maxMessage: "First name cannot be longer than {{ limit }} characters" - lastName: - type: "String!" - description: "Customer user last name" - validation: - - NotBlank: - message: "Please enter last name" - - Length: - max: 100 - maxMessage: "Last name cannot be longer than {{ limit }} characters" email: type: "String!" - description: "Customer user email." + description: "The customer's email address" validation: - NotBlank: message: "Please enter email" @@ -37,13 +24,20 @@ RegistrationDataInputDecorator: message: "This email is already registered" password: type: "Password!" - description: "Customer user password." + description: "Customer user password" validation: - NotBlank: - message: "Please enter password" + message: "Please enter new password" - Length: min: 6 minMessage: "Password must be at least {{ limit }} characters long" + newsletterSubscription: + type: "Boolean!" + description: "Whether customer user should receive newsletters or not" + cartUuid: + type: "Uuid" + description: "Uuid of the cart that should be merged to the cart of the newly registered user" + defaultValue: null productListsUuids: type: "[Uuid!]!" description: "Uuids of product lists that should be merged to the product lists of the user after registration" diff --git a/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/TelephoneInputObjectDecorator.types.yaml b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/TelephoneInputObjectDecorator.types.yaml new file mode 100644 index 0000000000..dcaa4c50bd --- /dev/null +++ b/src/Resources/config/graphql-types/ModelType/Customer/CustomerUser/Input/TelephoneInputObjectDecorator.types.yaml @@ -0,0 +1,19 @@ +TelephoneInputObjectDecorator: + type: input-object + decorator: true + config: + fields: + telephone: + type: "String!" + description: "The customer's telephone number" + validation: + - NotBlank: + message: "Please enter telephone number" + - Length: + min: 9 + minMessage: "Telephone number cannot be shorter than {{ limit }} characters" + max: 30 + maxMessage: "Telephone number cannot be longer than {{ limit }} characters" + - Regex: + pattern: '/^[0-9\+]+$/' + message: "Please enter only numbers and the + sign" diff --git a/src/Resources/config/graphql-types/Mutation/MutationDecorator.types.yaml b/src/Resources/config/graphql-types/Mutation/MutationDecorator.types.yaml index 0d9b48b706..995fb3eecf 100644 --- a/src/Resources/config/graphql-types/Mutation/MutationDecorator.types.yaml +++ b/src/Resources/config/graphql-types/Mutation/MutationDecorator.types.yaml @@ -47,13 +47,21 @@ MutationDecorator: access: "@=isGranted('ROLE_API_ALL')" resolve: "@=mutation('changePersonalDataMutation', args, validator)" Register: - type: Token! + type: 'LoginResult!' description: "Register new customer user" args: input: type: RegistrationDataInput! validation: cascade resolve: "@=mutation('registerMutation', args, validator)" + RegisterByOrder: + type: 'LoginResult!' + description: "Register new customer user using an order data" + args: + input: + type: RegistrationByOrderInput! + validation: cascade + resolve: "@=mutation('registerByOrderMutation', args)" NewsletterSubscribe: type: Boolean! description: "Subscribe for e-mail newsletter" diff --git a/src/Resources/translations/validators.cs.po b/src/Resources/translations/validators.cs.po index 9498443cad..14368a6abc 100644 --- a/src/Resources/translations/validators.cs.po +++ b/src/Resources/translations/validators.cs.po @@ -127,8 +127,11 @@ msgstr "Zadejte staré heslo" msgid "Please enter only numbers and the + character" msgstr "Prosím, zadávejte pouze čísla a znak +" -msgid "Please enter password" -msgstr "Vyplňte prosím heslo" +msgid "Please enter only numbers and the + sign" +msgstr "Prosím vyplňte čísla a znak +" + +msgid "Please enter order URL hash" +msgstr "Vyplňte prosím URL hash objednávky" msgid "Please enter quantity" msgstr "Zadejte prosím množství" diff --git a/src/Resources/translations/validators.en.po b/src/Resources/translations/validators.en.po index d438aa5e32..1e854abe4d 100644 --- a/src/Resources/translations/validators.en.po +++ b/src/Resources/translations/validators.en.po @@ -127,7 +127,10 @@ msgstr "" msgid "Please enter only numbers and the + character" msgstr "" -msgid "Please enter password" +msgid "Please enter only numbers and the + sign" +msgstr "" + +msgid "Please enter order URL hash" msgstr "" msgid "Please enter quantity"