diff --git a/src/Authenticator/JWTGuestOrderAuthenticator.php b/src/Authenticator/JWTGuestOrderAuthenticator.php new file mode 100644 index 000000000..3c62038c7 --- /dev/null +++ b/src/Authenticator/JWTGuestOrderAuthenticator.php @@ -0,0 +1,72 @@ +headers->has(self::TOKEN_HEADER); + } + + public function getCredentials(Request $request): string + { + return $request->headers->get(self::TOKEN_HEADER); + } + + public function getUser($credentials, UserProviderInterface $userProvider): UserInterface + { + try { + return $userProvider->loadUserByUsername($credentials); + } catch (JWTDecodeFailureException $decodeFailureException) { + throw new AuthenticationException($decodeFailureException->getMessage()); + } + } + + public function checkCredentials($credentials, UserInterface $user): bool + { + return true; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + return new JsonResponse(['message' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response + { + // The request continues + return null; + } + + public function supportsRememberMe(): bool + { + return false; + } + + public function start(Request $request, AuthenticationException $authException = null): Response + { + return new JsonResponse(['message' => 'Authentication required'], Response::HTTP_UNAUTHORIZED); + } + + public function createAuthenticatedToken(UserInterface $user, $providerKey): GuardTokenInterface + { + return new PostAuthenticationGuardToken($user, $providerKey, []); + } +} diff --git a/src/Controller/Customer/GuestLoginAction.php b/src/Controller/Customer/GuestLoginAction.php new file mode 100644 index 000000000..0a9964061 --- /dev/null +++ b/src/Controller/Customer/GuestLoginAction.php @@ -0,0 +1,99 @@ +viewHandler = $viewHandler; + $this->validator = $validator; + $this->validationErrorViewFactory = $validationErrorViewFactory; + $this->orderRepository = $orderRepository; + $this->guestOrderJWTEncoder = $guestOrderJWTEncoder; + } + + public function __invoke(Request $request): Response + { + // This is just to validate that all necessary fields are present. + $loginRequest = new GuestLoginRequest($request); + $validationErrors = $this->validator->validate($loginRequest); + if (0 < count($validationErrors)) { + return $this->viewHandler->handle( + View::create( + $this->validationErrorViewFactory->create($validationErrors), + Response::HTTP_BAD_REQUEST + ) + ); + } + + // Actual login logic + $success = false; + + /** @var OrderInterface $order */ + $order = $this->orderRepository->findOneByNumber($loginRequest->getOrderNumber()); + + // The order has to exist and must be placed + if (null !== $order && $order->getCheckoutState() !== OrderCheckoutStates::STATE_CART) { + /** @var CustomerInterface $customer */ + $customer = $order->getCustomer(); + $paymentMethod = $order->getLastPayment()->getMethod(); + + // The order must be a guest order. Also the provided email & payment method must match. + if ( + null === $customer->getUser() && + $loginRequest->getEmail() === $customer->getEmail() && + $paymentMethod->getCode() === $loginRequest->getPaymentMethodCode() + ) { + $success = true; + } + } + + // Return the jwt on success + if (true === $success) { + $jwt = $this->guestOrderJWTEncoder->encode($order); + + return $this->viewHandler->handle(View::create(['jwt' => $jwt], Response::HTTP_OK)); + } + + return $this->viewHandler->handle(View::create(['message' => 'Bad credentials.'], Response::HTTP_UNAUTHORIZED)); + } +} diff --git a/src/Controller/Order/ShowGuestOrderDetailsAction.php b/src/Controller/Order/ShowGuestOrderDetailsAction.php new file mode 100644 index 000000000..e07f53919 --- /dev/null +++ b/src/Controller/Order/ShowGuestOrderDetailsAction.php @@ -0,0 +1,59 @@ +viewHandler = $viewHandler; + $this->tokenStorage = $tokenStorage; + $this->placedOrderViewRepository = $placedOrderViewRepository; + } + + public function __invoke(Request $request): Response + { + try { + $token = $this->tokenStorage->getToken(); + + Assert::notNull($token); + + /** @var CustomerGuestAuthenticationInterface|CustomerInterface $customer */ + $customer = $token->getUser(); + + Assert::isInstanceOf($customer, CustomerInterface::class); + Assert::isInstanceOf($customer, CustomerGuestAuthenticationInterface::class); + Assert::null($customer->getUser()); + + $order = $this->placedOrderViewRepository->getOneCompletedByCustomerEmailAndToken($customer->getEmail(), $customer->getAuthorizedOrder()->getTokenValue()); + } catch (\InvalidArgumentException $exception) { + return $this->viewHandler->handle(View::create(null, Response::HTTP_UNAUTHORIZED)); + } + + return $this->viewHandler->handle(View::create($order, Response::HTTP_OK)); + } +} diff --git a/src/Encoder/GuestOrderJWTEncoder.php b/src/Encoder/GuestOrderJWTEncoder.php new file mode 100644 index 000000000..ed4f0942c --- /dev/null +++ b/src/Encoder/GuestOrderJWTEncoder.php @@ -0,0 +1,41 @@ +JWTEncoder = $JWTEncoder; + $this->orderRepository = $orderRepository; + } + + public function encode(OrderInterface $order): string + { + $data = ['orderToken' => $order->getTokenValue()]; + + return $this->JWTEncoder->encode($data); + } + + public function decode(string $jwt): OrderInterface + { + $data = $this->JWTEncoder->decode($jwt); + + /** @var OrderInterface $order */ + $order = $this->orderRepository->findOneByTokenValue($data['orderToken']); + + return $order; + } +} diff --git a/src/Encoder/GuestOrderJWTEncoderInterface.php b/src/Encoder/GuestOrderJWTEncoderInterface.php new file mode 100644 index 000000000..6b2ad7df0 --- /dev/null +++ b/src/Encoder/GuestOrderJWTEncoderInterface.php @@ -0,0 +1,14 @@ +encoder = $encoder; + } + + public function loadUserByUsername($jwt): UserInterface + { + $order = $this->encoder->decode($jwt); + + /** @var CustomerGuestAuthenticationInterface $customer */ + $customer = $order->getCustomer(); + Assert::implementsInterface($customer, CustomerGuestAuthenticationInterface::class); + + $customer->setAuthorizedOrder($order); + + return $customer; + } + + public function refreshUser(UserInterface $user) + { + } + + public function supportsClass($class): bool + { + return (new ReflectionClass($class))->implementsInterface(CustomerGuestAuthenticationInterface::class); + } +} diff --git a/src/Request/Customer/GuestLoginRequest.php b/src/Request/Customer/GuestLoginRequest.php new file mode 100644 index 000000000..6d638c57e --- /dev/null +++ b/src/Request/Customer/GuestLoginRequest.php @@ -0,0 +1,41 @@ +email = $request->request->get('email'); + $this->orderNumber = $request->request->get('orderNumber'); + $this->paymentMethodCode = $request->request->get('paymentMethod'); + } + + public function getEmail(): string + { + return $this->email; + } + + public function getOrderNumber(): string + { + return $this->orderNumber; + } + + public function getPaymentMethodCode(): string + { + return $this->paymentMethodCode; + } +} diff --git a/src/Resources/config/routing/order.yml b/src/Resources/config/routing/order.yml index fa2215128..7e613e5c0 100644 --- a/src/Resources/config/routing/order.yml +++ b/src/Resources/config/routing/order.yml @@ -9,3 +9,9 @@ sylius_shop_api_order_details: methods: [GET] defaults: _controller: sylius.shop_api_plugin.controller.order.show_order_details_action + +sylius_shop_api_guest_order_details: + path: /guest/order + methods: [GET] + defaults: + _controller: sylius.shop_api_plugin.controller.order.show_guest_order_details_action diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 01d5bca41..0a8ebe698 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -13,8 +13,10 @@ + + diff --git a/src/Resources/config/services/actions/customer.xml b/src/Resources/config/services/actions/customer.xml index 4a241f8d9..9e6eb5218 100644 --- a/src/Resources/config/services/actions/customer.xml +++ b/src/Resources/config/services/actions/customer.xml @@ -67,5 +67,15 @@ + + + + + + + + diff --git a/src/Resources/config/services/actions/order.xml b/src/Resources/config/services/actions/order.xml index e0d38092c..8aec1d64c 100644 --- a/src/Resources/config/services/actions/order.xml +++ b/src/Resources/config/services/actions/order.xml @@ -19,5 +19,13 @@ + + + + + + diff --git a/src/Resources/config/services/authenticator.xml b/src/Resources/config/services/authenticator.xml new file mode 100644 index 000000000..ee89397f9 --- /dev/null +++ b/src/Resources/config/services/authenticator.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/Resources/config/services/encoder.xml b/src/Resources/config/services/encoder.xml new file mode 100644 index 000000000..243890543 --- /dev/null +++ b/src/Resources/config/services/encoder.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/Resources/config/services/providers.xml b/src/Resources/config/services/providers.xml index 82a87f51e..f086df420 100644 --- a/src/Resources/config/services/providers.xml +++ b/src/Resources/config/services/providers.xml @@ -23,5 +23,10 @@ + + + + diff --git a/src/Resources/config/validation/customer/GuestLoginRequest.xml b/src/Resources/config/validation/customer/GuestLoginRequest.xml new file mode 100644 index 000000000..429867391 --- /dev/null +++ b/src/Resources/config/validation/customer/GuestLoginRequest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Traits/CustomerGuestAuthenticationInterface.php b/src/Traits/CustomerGuestAuthenticationInterface.php new file mode 100644 index 000000000..fe1de7283 --- /dev/null +++ b/src/Traits/CustomerGuestAuthenticationInterface.php @@ -0,0 +1,15 @@ +authorizedOrder; + } + + public function setAuthorizedOrder(OrderInterface $authorizedOrder): void + { + $this->authorizedOrder = $authorizedOrder; + } + + /* + * The following methods are required by the UserInterface, which we need to be able to allow a guest login + */ + + /** {@inheritdoc} */ + public function getRoles() + { + return []; + } + + /** {@inheritdoc} */ + public function getPassword() + { + return ''; + } + + /** {@inheritdoc} */ + public function getSalt() + { + return null; + } + + /** {@inheritdoc} */ + public function getUsername() + { + // Generate a unique temporary username + return sprintf('guest_%s_%s', str_replace('@', '(at)', $this->email), Uuid::uuid4()->toString()); + } + + /** {@inheritdoc} */ + public function eraseCredentials() + { + } +} diff --git a/tests/Application/config/packages/_sylius.yaml b/tests/Application/config/packages/_sylius.yaml index 04471fac0..8acaa1d09 100644 --- a/tests/Application/config/packages/_sylius.yaml +++ b/tests/Application/config/packages/_sylius.yaml @@ -9,3 +9,9 @@ imports: parameters: sylius_core.public_dir: '%kernel.project_dir%/public' + +sylius_customer: + resources: + customer: + classes: + model: 'Tests\Sylius\ShopApiPlugin\Application\src\Entity\Customer' diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml index f51ba5a22..41ee20f5a 100644 --- a/tests/Application/config/packages/doctrine.yaml +++ b/tests/Application/config/packages/doctrine.yaml @@ -12,3 +12,13 @@ doctrine: charset: UTF8 url: '%env(resolve:DATABASE_URL)%' + + orm: + entity_managers: + default: + mappings: + custom_mapping: + type: 'xml' + prefix: 'Tests\Sylius\ShopApiPlugin\Application\src\Entity' + dir: '%kernel.root_dir%/src/Resources/config/doctrine' + is_bundle: false diff --git a/tests/Application/config/packages/security.yaml b/tests/Application/config/packages/security.yaml index b08090846..088e70d0b 100644 --- a/tests/Application/config/packages/security.yaml +++ b/tests/Application/config/packages/security.yaml @@ -9,6 +9,8 @@ security: id: sylius.admin_user_provider.email_or_name_based sylius_shop_user_provider: id: sylius.shop_user_provider.email_or_name_based + sylius_guest_user_provider: + id: sylius.shop_api_plugin.provider.guest_user_provider encoders: Sylius\Component\User\Model\UserInterface: sha512 firewalls: @@ -49,6 +51,21 @@ security: fos_oauth: true stateless: true anonymous: true + + shop_api_guest: + pattern: "%sylius_shop_api.security.regex%/[^/]+/guest" + stateless: true + anonymous: true + provider: sylius_guest_user_provider + form_login: + check_path: /shop-api/login/guest + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + require_previous_session: false + guard: + provider: sylius_guest_user_provider + authenticators: + - sylius.shop_api_plugin.authenticator.jwtguest_order_authenticator sylius_shop_api: pattern: "%sylius_shop_api.security.regex%" @@ -61,6 +78,7 @@ security: password_path: password success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure + require_previous_session: false guard: authenticators: - lexik_jwt_authentication.jwt_token_authenticator diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml index 2a86cead8..e0890438d 100644 --- a/tests/Application/config/routes.yaml +++ b/tests/Application/config/routes.yaml @@ -6,3 +6,9 @@ sylius_shop_api: sylius_shop_api_login_check: methods: [POST] path: /shop-api/login_check + +sylius_shop_api_guest_login_check: + methods: [POST] + path: /shop-api/guest/login_check + defaults: + _controller: sylius.shop_api_plugin.controller.customer.guest_login_action diff --git a/tests/Application/src/Entity/Customer.php b/tests/Application/src/Entity/Customer.php new file mode 100644 index 000000000..47a9efdd9 --- /dev/null +++ b/tests/Application/src/Entity/Customer.php @@ -0,0 +1,15 @@ + + + + diff --git a/tests/Controller/Customer/GuestLoginApiTest.php b/tests/Controller/Customer/GuestLoginApiTest.php new file mode 100644 index 000000000..39fac2006 --- /dev/null +++ b/tests/Controller/Customer/GuestLoginApiTest.php @@ -0,0 +1,66 @@ +loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + $data = <<client->request(Request::METHOD_POST, '/shop-api/guest/login_check', [], [], self::CONTENT_TYPE_HEADER, $data); + $response = $this->client->getResponse(); + $this->assertResponse($response, 'customer/guest_login_response'); + } + + /** @test */ + public function it_returns_unauthorized_on_wrong_payment_method(): void + { + $this->loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + $data = <<client->request(Request::METHOD_POST, '/shop-api/guest/login_check', [], [], self::CONTENT_TYPE_HEADER, $data); + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED); + } + + /** @test */ + public function it_returns_unauthorized_if_order_belongs_to_user(): void + { + $this->loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + $data = <<client->request(Request::METHOD_POST, '/shop-api/guest/login_check', [], [], self::CONTENT_TYPE_HEADER, $data); + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED); + } +} diff --git a/tests/Controller/Order/GuestOrderShowApiTest.php b/tests/Controller/Order/GuestOrderShowApiTest.php new file mode 100644 index 000000000..cf91f6738 --- /dev/null +++ b/tests/Controller/Order/GuestOrderShowApiTest.php @@ -0,0 +1,60 @@ +loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + /** @var GuestOrderJWTEncoderInterface $encoder */ + $encoder = $this->get('sylius.shop_api_plugin.encoder.guest_order_jwtencoder'); + + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->get('sylius.repository.order'); + + $token = 'GUEST_ORDER_TOKEN'; + $order = $orderRepository->findOneByTokenValue($token); + $jwt = $encoder->encode($order); + + $this->client->setServerParameter(self::GUEST_TOKEN_HEADER, $jwt); + $this->client->request(Request::METHOD_GET, '/shop-api/guest/order'); + $response = $this->client->getResponse(); + $this->assertResponse($response, 'order/guest_order_summary_response'); + } + + /** @test */ + public function it_returns_unauthorized_if_no_jwt_provided(): void + { + $this->loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + $this->client->request(Request::METHOD_GET, '/shop-api/guest/order'); + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED); + } + + /** @test */ + public function it_returns_unauthorized_if_invalid_jwt_provided(): void + { + $this->loadFixturesFromFiles(['shop.yml', 'order.yml', 'customer.yml', 'address.yml']); + + $jwt = 'abc'; + + $this->client->setServerParameter(self::GUEST_TOKEN_HEADER, $jwt); + $this->client->request(Request::METHOD_GET, '/shop-api/guest/order'); + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_UNAUTHORIZED); + } +} diff --git a/tests/DataFixtures/ORM/address.yml b/tests/DataFixtures/ORM/address.yml index 33d73bd8f..3e3384a40 100644 --- a/tests/DataFixtures/ORM/address.yml +++ b/tests/DataFixtures/ORM/address.yml @@ -43,3 +43,13 @@ Sylius\Component\Core\Model\Address: city: "Split" postcode: "21000" customer: "@hater" + customer_guest_address: + firstName: "John" + lastName: "Doe" + phoneNumber: 0912539021 + countryCode: "GB" + provinceCode: "GB-SCT" + street: "Example str. 123" + city: "London" + postcode: "21000" + customer: "@guest" diff --git a/tests/DataFixtures/ORM/customer.yml b/tests/DataFixtures/ORM/customer.yml index f6aca3428..d18ab4940 100644 --- a/tests/DataFixtures/ORM/customer.yml +++ b/tests/DataFixtures/ORM/customer.yml @@ -15,7 +15,7 @@ Sylius\Component\Core\Model\ShopUser: username: "hater@queen.com" usernameCanonical: "hater@queen.com" -Sylius\Component\Core\Model\Customer: +Tests\Sylius\ShopApiPlugin\Application\src\Entity\Customer: customer_oliver: firstName: "Oliver" lastName: "Queen" @@ -31,6 +31,10 @@ Sylius\Component\Core\Model\Customer: email: "hater@queen.com" emailCanonical: "slade@queen.com" + guest: + email: "john@doe.com" + emailCanonical: "john@doe.com" + Sylius\Component\Customer\Model\CustomerGroup: retail: name: "Retail" diff --git a/tests/DataFixtures/ORM/order.yml b/tests/DataFixtures/ORM/order.yml new file mode 100644 index 000000000..068d31ee8 --- /dev/null +++ b/tests/DataFixtures/ORM/order.yml @@ -0,0 +1,68 @@ +Sylius\Component\Core\Model\Order: + olivers_order: + number: "00000022" + state: "new" + customer: "@customer_oliver" + channel: "@gb_web_channel" + shippingAddress: "@customer_oliver_home_address" + billingAddress: "@customer_oliver_home_address" + currencyCode: "GBP" + localeCode: "en_GB" + checkoutState: "completed" + paymentState: "awaiting_payment" + shippingState: "ready" + tokenValue: "ORDER_TOKEN" + items: ["@olivers_order_item"] + __calls: + - addPayment: ["@olivers_payment"] + + guest_order: + channel: "@gb_web_channel" + number: "00000078" + state: "new" + customer: "@guest" + shippingAddress: "@customer_guest_address" + billingAddress: "@customer_guest_address" + currencyCode: "GBP" + localeCode: "en_GB" + checkoutState: "completed" + paymentState: "awaiting_payment" + shippingState: "ready" + tokenValue: "GUEST_ORDER_TOKEN" + checkoutCompletedAt: "" + __calls: + - addPayment: ["@guest_payment"] + +Sylius\Component\Core\Model\OrderItem: + olivers_order_item: + order: "@olivers_order" + unitPrice: 1999 + variant: "@mug_variant" + productName: "Logan Mug" + variantName: "Logan Mug" + +Sylius\Component\Core\Model\OrderItemUnit: + olivers_glasses_order_item_unit: + __construct: ["@olivers_order_item"] + +Sylius\Component\Core\Model\Payment: + olivers_payment: + order: "@olivers_order" + method: "@bank_payment" + currencyCode: "GBP" + amount: 1999 + state: "new" + guest_payment: + order: "@guest_order" + method: "@bank_payment" + currencyCode: "GBP" + amount: 1000 + state: "new" + +Sylius\Component\Core\Model\PaymentMethod: + bank_payment: + channels: ["@gb_web_channel"] + code: "bank_payment" + environment: "test" + currentLocale: "en_GB" + name: "Bank Payment" diff --git a/tests/DataFixtures/ORM/shop.yml b/tests/DataFixtures/ORM/shop.yml index 30a3777a1..18ec442d7 100644 --- a/tests/DataFixtures/ORM/shop.yml +++ b/tests/DataFixtures/ORM/shop.yml @@ -34,6 +34,15 @@ Sylius\Component\Locale\Model\Locale: locale_de_de: code: de_DE +Doctrine\Common\Collections\ArrayCollection: + mug_attributes: + __construct: + 0: + - "@en_gb_mug_holiday_collection_value" + - "@de_de_mug_holiday_collection_value" + - "@en_gb_mug_holiday_material_value" + - "@de_de_mug_holiday_material_value" + Sylius\Component\Core\Model\Product: mug: code: "LOGAN_MUG_CODE" @@ -44,11 +53,7 @@ Sylius\Component\Core\Model\Product: images: ["@mug_thumbnail"] mainTaxon: "@mug_taxon" productTaxons: ["@mug_product_taxon"] - attributes: - - "@en_gb_mug_holiday_collection_value" - - "@de_de_mug_holiday_collection_value" - - "@en_gb_mug_holiday_material_value" - - "@de_de_mug_holiday_material_value" + attributes: "@mug_attributes" t_shirt: code: "LOGAN_T_SHIRT_CODE" createdAt: "" diff --git a/tests/Responses/Expected/customer/guest_login_response.json b/tests/Responses/Expected/customer/guest_login_response.json new file mode 100644 index 000000000..661b030c8 --- /dev/null +++ b/tests/Responses/Expected/customer/guest_login_response.json @@ -0,0 +1,3 @@ +{ + "jwt": @string@ +} diff --git a/tests/Responses/Expected/order/guest_order_summary_response.json b/tests/Responses/Expected/order/guest_order_summary_response.json new file mode 100644 index 000000000..1b47be458 --- /dev/null +++ b/tests/Responses/Expected/order/guest_order_summary_response.json @@ -0,0 +1,52 @@ +{ + "channel": "WEB_GB", + "currency": "GBP", + "locale": "en_GB", + "checkoutState": "completed", + "checkoutCompletedAt": @string@, + "items": [], + "totals": { + "total": 0, + "items": 0, + "taxes": 0, + "shipping": 0, + "promotion": 0 + }, + "shippingAddress": { + "firstName": "John", + "lastName": "Doe", + "countryCode": "GB", + "street": "Example str. 123", + "city": "London", + "postcode": "21000", + "phoneNumber": "349713" + }, + "billingAddress": { + "firstName": "John", + "lastName": "Doe", + "countryCode": "GB", + "street": "Example str. 123", + "city": "London", + "postcode": "21000", + "phoneNumber": "349713" + }, + "payments": [ + { + "state": "new", + "method": { + "code": "bank_payment", + "name": "Bank Payment", + "description": @string@, + "instructions": @string@ + }, + "price": { + "current": 1000, + "currency": "GBP" + } + } + ], + "shipments": [], + "cartDiscounts": [], + "tokenValue": "GUEST_ORDER_TOKEN", + "number": "00000078" +}