From 0f6966a24635a39522d3c34ce787ea8250998a8b Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Wed, 4 Oct 2017 21:53:24 +0300 Subject: [PATCH 01/22] [Product Reviews API] - add API for product reviews --- .../UpdateProductReviewStatusController.php | 106 ++++++++++++++++++ .../Resources/config/app/config.yml | 1 + .../Resources/config/grids/product_review.yml | 46 ++++++++ .../Resources/config/routing/main.yml | 4 + .../config/routing/product_review.yml | 84 ++++++++++++++ .../Resources/config/services/controller.xml | 4 + .../Doctrine/ORM/ProductReviewRepository.php | 32 ++++++ .../ProductReviewRepositoryInterface.php | 18 +++ 8 files changed, 295 insertions(+) create mode 100644 src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml diff --git a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php new file mode 100644 index 00000000000..09dafb218c8 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php @@ -0,0 +1,106 @@ + + */ +final class UpdateProductReviewStatusController +{ + /** + * @var ProductReviewRepositoryInterface + */ + private $productReviewRepository; + + /** + * @var EntityManagerInterface + */ + private $manager; + + /** + * @param RepositoryInterface $productReviewRepository + * @param EntityManagerInterface $manager + */ + public function __construct( + RepositoryInterface $productReviewRepository, + EntityManagerInterface $manager + ) { + $this->productReviewRepository = $productReviewRepository; + $this->manager = $manager; + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function acceptProductReviewAction(Request $request): JsonResponse + { + $reviewId = $request->get('id'); + $productCode = $request->get('productCode'); + + if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** @var ReviewInterface $productReview */ + $productReview = $this->productReviewRepository->findOneByIdAndProductCode( + $reviewId, + $productCode + ); + + $productReview->setStatus(ReviewInterface::STATUS_ACCEPTED); + + $this->manager->persist($productReview); + $this->manager->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function rejectProductReviewAction(Request $request): JsonResponse + { + $reviewId = $request->get('id'); + $productCode = $request->get('productCode'); + + if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** @var ReviewInterface $productReview */ + $productReview = $this->productReviewRepository->findOneByIdAndProductCode( + $reviewId, + $productCode + ); + + $productReview->setStatus(ReviewInterface::STATUS_REJECTED); + + $this->manager->persist($productReview); + $this->manager->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml index 723b525cfcb..c3c07d6a1b4 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml @@ -8,6 +8,7 @@ imports: - { resource: "@SyliusAdminApiBundle/Resources/config/grids/payments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } + - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_review.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/promotion.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/shipments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/taxon.yml" } diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml new file mode 100644 index 00000000000..36d5f1bd866 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml @@ -0,0 +1,46 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski +# +# @author Paul Stoica + +sylius_grid: + grids: + sylius_admin_api_product_review: + driver: + name: doctrine/orm + options: + class: "%sylius.model.product_review.class%" + repository: + method: createQueryBuilderByProductCode + arguments: ["%locale%", $productCode] + sorting: + date: desc + fields: + date: + type: datetime + label: sylius.ui.date + path: createdAt + sortable: createdAt + options: + format: d-m-Y H:i:s + title: + type: string + label: sylius.ui.title + sortable: ~ + rating: + type: string + label: sylius.ui.rating + sortable: ~ + status: + type: twig + label: sylius.ui.status + reviewSubject: + type: string + label: sylius.ui.product + author: + type: string + label: sylius.ui.customer + filters: + title: + type: string + label: sylius.ui.title diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml index 47b0cb85d8e..f7d334ec370 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml @@ -67,6 +67,10 @@ sylius_api_product_variant: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_variant.yml" prefix: /products/{productCode} +sylius_api_product_review: + resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" + prefix: /products/{productCode} + sylius_api_promotion: resource: "@SyliusAdminApiBundle/Resources/config/routing/promotion.yml" diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml new file mode 100644 index 00000000000..429d3bd9595 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -0,0 +1,84 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski +# +# @author Paul Stoica + +sylius_admin_api_product_review: + resource: | + path: 'reviews' + grid: sylius_admin_api_product_review + alias: sylius.product_review + section: admin_api + only: ['index'] + serialization_version: $version + vars: + route: + parameters: + productCode: $productCode + type: sylius.resource_api + +sylius_admin_api_product_review_create: + path: /reviews/ + methods: [POST] + defaults: + _controller: sylius.controller.product_review:createAction + _sylius: + serialization_groups: [Default, Detailed] + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + factory: + method: createForSubject + arguments: + - expr:notFoundOnNull(service('sylius.repository.product').findOneByCode($productCode)) + +sylius_admin_api_product_review_update: + path: /reviews/{id} + methods: [PUT, PATCH] + defaults: + _controller: sylius.controller.product_review:updateAction + _sylius: + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + +sylius_admin_api_product_review_delete: + path: /reviews/{id} + methods: [DELETE] + defaults: + _controller: sylius.controller.product_review:deleteAction + _sylius: + serialization_version: $version + section: admin_api + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + csrf_protection: false + +sylius_admin_api_product_review_show: + path: /reviews/{code} + methods: [GET] + defaults: + _controller: sylius.controller.product_review:showAction + _sylius: + serialization_version: $version + section: admin_api + serialization_groups: [Default, Detailed] + repository: + method: findOneByIdAndProductCode + arguments: [$code, $productCode] + +sylius_admin_api_product_review_accept: + path: /reviews/{id}/accept + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.update_product_review_status:acceptProductReviewAction + +sylius_admin_api_product_review_reject: + path: /reviews/{id}/reject + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.update_product_review_status:rejectProductReviewAction \ No newline at end of file diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml index 4c21b104799..d6fd87b4d9a 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml @@ -30,5 +30,9 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 3b27935b430..288d76d2447 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -13,6 +13,7 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; +use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductReviewRepositoryInterface; @@ -20,6 +21,7 @@ /** * @author Mateusz Zalewski + * @author Paul Stoica */ class ProductReviewRepository extends EntityRepository implements ProductReviewRepositoryInterface { @@ -60,4 +62,34 @@ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale ->getResult() ; } + + /** + * {@inheritdoc} + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder + { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->innerJoin('product.translations', 'translation') + ->andWhere('translation.locale = :locale') + ->andWhere('product.code = :productCode') + ->setParameter('locale', $locale) + ->setParameter('productCode', $productCode) + ; + } + + /** + * {@inheritdoc} + */ + public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->andWhere('product.code = :productCode') + ->andWhere('o.id = :id') + ->setParameter('productCode', $productCode) + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult() + ; + } } diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index ee68e0c9512..019ad5dda66 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -13,12 +13,14 @@ namespace Sylius\Component\Core\Repository; +use Doctrine\ORM\QueryBuilder; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Review\Model\ReviewInterface; /** * @author Mateusz Zalewski + * @author Paul Stoica */ interface ProductReviewRepositoryInterface extends RepositoryInterface { @@ -38,4 +40,20 @@ public function findLatestByProductId($productId, int $count): array; * @return array|ReviewInterface[] */ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale, ChannelInterface $channel): array; + + /** + * @param string $locale + * @param string $productCode + * + * @return QueryBuilder + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder; + + /** + * @param integer $id + * @param string $productCode + * + * @return ReviewInterface|null + */ + public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface; } From c660fa9941435463ab2eaf08bd0c9f82b53d8261 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Wed, 4 Oct 2017 22:39:50 +0300 Subject: [PATCH 02/22] [Documentation][API] - add product reviews api docs --- docs/api/product_reviews.rst | 637 +++++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 docs/api/product_reviews.rst diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst new file mode 100644 index 00000000000..77ea810df36 --- /dev/null +++ b/docs/api/product_reviews.rst @@ -0,0 +1,637 @@ +Product Reviews API +==================== + +These endpoints will allow you to easily manage product reviews. Base URI is `/api/v1/products/{productCode}/reviews/`. + +Product Reviews API response structure +-------------------------------------- + +When you get a collection of resources, you will receive objects with the following fields: + ++------------------+------------------------------------------------------------------------------------------------+ +| Field | Description | ++==================+================================================================================================+ +| id | Id of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| title | Title of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| comment | Comment of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| author | Customer author for product review (This is customer that added the | +| | product review; this will contain customer resource information) | ++------------------+------------------------------------------------------------------------------------------------+ +| status | Status of product review(New, Accepted, Rejected) | ++------------------+------------------------------------------------------------------------------------------------+ +| reviewSubject | This is the review subject for the product review. For this case of the product review, this | +| | will contains a product resource | ++------------------+------------------------------------------------------------------------------------------------+ + +.. note:: + + Read more about :doc:`ProductReviews docs`. + +Creating a Product Review +-------------------------- + +To create a new product review you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``POST`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/ + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be created | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating(1..5) | ++---------------+----------------+----------------------------------------------------------+ +| author | request | Product review author | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To create new product review for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST \ + --data ' + { + "title": "A product review", + "rating": "3", + "comment": "This is a comment review", + "author": { + "email": "test@example.com" + } + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 201 Created + +.. code-block:: json + + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + + +.. warning:: + + If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. + +Example +^^^^^^^ + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 400 Bad Request + +.. code-block:: json + + { + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } + } + + +Getting a Single Product Review +-------------------------------- + +To retrieve the details of a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To see the details of the product review with ``id = 1``, which is defined for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "id": 1, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + +Collection of Product Reviews +------------------------------ + +To retrieve a paginated list of reviews for a selected product you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/ + ++-------------------------------------+----------------+------------------------------------------------------------+ +| Parameter | Parameter type | Description | ++=====================================+================+============================================================+ +| Authorization | header | Token received during authentication | ++-------------------------------------+----------------+------------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++-------------------------------------+----------------+------------------------------------------------------------+ +| limit | query | *(optional)* Number of items to display per page, | +| | | by default = 10 | ++-------------------------------------+----------------+------------------------------------------------------------+ +| sorting['nameOfField']['direction'] | query | *(optional)* Field and direction of sorting, | +| | | by default 'desc' and 'createdAt' | ++-------------------------------------+----------------+------------------------------------------------------------+ + +Example +^^^^^^^ + +To see the first page of all product reviews for the product with ``code = MUG-TH`` use the method below. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "page": 1, + "limit": 10, + "pages": 1, + "total": 3, + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "first": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "last": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + } + }, + "_embedded": { + "items": [ + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T20:19:06+03:00", + "updatedAt": "2017-10-04T20:19:06+03:00" + }, + { + "id": 3, + "title": "A product review 2", + "rating": 5, + "comment": "This is a comment review 2", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T18:23:56+03:00", + "updatedAt": "2017-10-04T18:44:08+03:00" + }, + { + "id": 1, + "title": "Test review 3", + "rating": 4, + "comment": "This is a comment review 3", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "accepted", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-03T23:53:24+03:00", + "updatedAt": "2017-10-04T19:18:00+03:00" + } + ] + } + } + +Updating Product Review +------------------------ + +To fully update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PUT`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PUT /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| id | url attribute | Product review id | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating(1..5) | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To fully update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PUT \ + --data ' + { + "title": "A product review", + "rating": "4", + "comment": "This is a comment for review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +To partially update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PATCH /api/v1/products/{productCode}/reviews/{id} + ++------------------------------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++====================================+================+===========================================================+ +| Authorization | header | Token received during authentication | ++------------------------------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++------------------------------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++------------------------------------+----------------+-----------------------------------------------------------+ +| title | request | Product review title | ++------------------------------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To partially update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PATCH \ + --data ' + { + "title": "This is an another title for the review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Deleting a Product Review +-------------------------- + +To delete a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``DELETE`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + DELETE /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be deleted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To delete the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X DELETE + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Accept a Product Review +-------------------------- + +To accept a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/accept`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/accept + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be accepted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To accept the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/accept \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Reject a Product Review +-------------------------- + +To reject a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/reject`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/reject + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be rejected | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To reject the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/reject \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + From fb337a6b3f0b73a3fe45295b55b29b141d604361 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:03:10 +0300 Subject: [PATCH 03/22] [Product Reviews API]: accept/reject endpoints with state machine --- .../UpdateProductReviewStatusController.php | 106 ------------------ .../config/routing/product_review.yml | 12 +- .../Resources/config/services/controller.xml | 4 - 3 files changed, 10 insertions(+), 112 deletions(-) delete mode 100644 src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php diff --git a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php deleted file mode 100644 index 09dafb218c8..00000000000 --- a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php +++ /dev/null @@ -1,106 +0,0 @@ - - */ -final class UpdateProductReviewStatusController -{ - /** - * @var ProductReviewRepositoryInterface - */ - private $productReviewRepository; - - /** - * @var EntityManagerInterface - */ - private $manager; - - /** - * @param RepositoryInterface $productReviewRepository - * @param EntityManagerInterface $manager - */ - public function __construct( - RepositoryInterface $productReviewRepository, - EntityManagerInterface $manager - ) { - $this->productReviewRepository = $productReviewRepository; - $this->manager = $manager; - } - - /** - * @param Request $request - * - * @return JsonResponse - */ - public function acceptProductReviewAction(Request $request): JsonResponse - { - $reviewId = $request->get('id'); - $productCode = $request->get('productCode'); - - if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** @var ReviewInterface $productReview */ - $productReview = $this->productReviewRepository->findOneByIdAndProductCode( - $reviewId, - $productCode - ); - - $productReview->setStatus(ReviewInterface::STATUS_ACCEPTED); - - $this->manager->persist($productReview); - $this->manager->flush(); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** - * @param Request $request - * - * @return JsonResponse - */ - public function rejectProductReviewAction(Request $request): JsonResponse - { - $reviewId = $request->get('id'); - $productCode = $request->get('productCode'); - - if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** @var ReviewInterface $productReview */ - $productReview = $this->productReviewRepository->findOneByIdAndProductCode( - $reviewId, - $productCode - ); - - $productReview->setStatus(ReviewInterface::STATUS_REJECTED); - - $this->manager->persist($productReview); - $this->manager->flush(); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } -} diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml index 429d3bd9595..b76b05db185 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -75,10 +75,18 @@ sylius_admin_api_product_review_accept: path: /reviews/{id}/accept methods: [POST, PUT, PATCH] defaults: - _controller: sylius.controller.update_product_review_status:acceptProductReviewAction + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: accept sylius_admin_api_product_review_reject: path: /reviews/{id}/reject methods: [POST, PUT, PATCH] defaults: - _controller: sylius.controller.update_product_review_status:rejectProductReviewAction \ No newline at end of file + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: reject \ No newline at end of file diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml index d6fd87b4d9a..4c21b104799 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml @@ -30,9 +30,5 @@ - - - - From 0e2ccc9e731e11dc6224f1ccf1e232b316d47e62 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:04:03 +0300 Subject: [PATCH 04/22] [Unit Testing]: ProductReviewApi Controller --- tests/Controller/ProductReviewApiTest.php | 359 ++++++++++++++++++ .../ORM/resources/product_reviews.yml | 42 ++ .../product_review/accept_response.json | 92 +++++ .../change_status_fail_response.json | 4 + .../product_review/create_response.json | 112 ++++++ .../create_validation_fail_response.json | 39 ++ .../product_review/index_response.json | 20 + .../product_review/reject_response.json | 92 +++++ .../product_review/show_response.json | 92 +++++ 9 files changed, 852 insertions(+) create mode 100644 tests/Controller/ProductReviewApiTest.php create mode 100644 tests/DataFixtures/ORM/resources/product_reviews.yml create mode 100644 tests/Responses/Expected/product_review/accept_response.json create mode 100644 tests/Responses/Expected/product_review/change_status_fail_response.json create mode 100644 tests/Responses/Expected/product_review/create_response.json create mode 100644 tests/Responses/Expected/product_review/create_validation_fail_response.json create mode 100644 tests/Responses/Expected/product_review/index_response.json create mode 100644 tests/Responses/Expected/product_review/reject_response.json create mode 100644 tests/Responses/Expected/product_review/show_response.json diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php new file mode 100644 index 00000000000..22fb356241b --- /dev/null +++ b/tests/Controller/ProductReviewApiTest.php @@ -0,0 +1,359 @@ + + */ +final class ProductReviewApiTest extends JsonApiTestCase +{ + /** + * @var array + */ + private static $authorizedHeaderWithContentType = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'CONTENT_TYPE' => 'application/json', + ]; + + /** + * @var array + */ + private static $authorizedHeaderWithAccept = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'ACCEPT' => 'application/json', + ]; + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_list_when_access_is_denied() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product)); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'authentication/access_denied_response', Response::HTTP_UNAUTHORIZED); + } + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_when_it_does_not_exist() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_showing_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/show_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_indexing_product_reviews() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/index_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_create_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $data = +<<client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_response', Response::HTTP_CREATED); + } + + /** + * @test + */ + public function it_does_not_allow_to_create_product_review_without_required_fields() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, []); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_validation_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_does_not_allow_delete_product_review_if_it_does_not_exist() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('DELETE', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_delete_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('DELETE', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, []); + + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_updating_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PUT', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_updating_partial_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('resources/locales.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PATCH', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_accept_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/accept_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_accept_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview2']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_allows_reject_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/reject', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/reject_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_reject_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview3']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @param ProductInterface $product + * + * @return string + */ + private function getReviewListUrl(ProductInterface $product) + { + return sprintf('/api/v1/products/%s/reviews/', $product->getCode()); + } + + /** + * @param ProductInterface $product + * @param ProductReview $productReview + * + * @return string + */ + private function getReviewUrl(ProductInterface $product, ProductReview $productReview) + { + return sprintf('%s%s', $this->getReviewListUrl($product), $productReview->getId()); + } +} diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml new file mode 100644 index 00000000000..6b16363a322 --- /dev/null +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -0,0 +1,42 @@ +Sylius\Component\Core\Model\Customer: + customer_example1: + firstName: Example + lastName: Queen + email: example.queen@example.com + emailCanonical: example.queen@example.com + customer_example2: + firstName: Example + lastName: King + email: example.king@example.com + emailCanonical: example.king@example.com +Sylius\Component\Core\Model\Product: + product1: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_BEST + product2: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_GOOD + +Sylius\Component\Core\Model\ProductReview: + productReview1: + title: mug_review_best + rating: 4 + comment: "mug_review_best_comment" + author: "@customer_example1" + reviewSubject: "@product1" + productReview2: + title: mug_review_good + rating: 3 + comment: "mug_review_good_comment" + author: "@customer_example1" + status: "rejected" + reviewSubject: "@product1" + productReview3: + title: mug_review_bad + rating: 1 + comment: "mug_review_bad_comment" + author: "@customer_example1" + status: "accepted" + reviewSubject: "@product1" \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json new file mode 100644 index 00000000000..b4765315c14 --- /dev/null +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 2.5, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/change_status_fail_response.json b/tests/Responses/Expected/product_review/change_status_fail_response.json new file mode 100644 index 00000000000..f0177c8ed52 --- /dev/null +++ b/tests/Responses/Expected/product_review/change_status_fail_response.json @@ -0,0 +1,4 @@ +{ + "code":400, + "message":"" +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json new file mode 100644 index 00000000000..a545063e07f --- /dev/null +++ b/tests/Responses/Expected/product_review/create_response.json @@ -0,0 +1,112 @@ +{ + "id": @integer@, + "title": "J_REVIEW", + "rating": 3, + "comment": "J_REVIEW_COMMENT", + "author": { + "id": @integer@, + "email": "j@example.com", + "emailCanonical": "j@example.com", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": [ + { + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + ], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/create_validation_fail_response.json b/tests/Responses/Expected/product_review/create_validation_fail_response.json new file mode 100644 index 00000000000..07e974598a9 --- /dev/null +++ b/tests/Responses/Expected/product_review/create_validation_fail_response.json @@ -0,0 +1,39 @@ +{ + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/index_response.json b/tests/Responses/Expected/product_review/index_response.json new file mode 100644 index 00000000000..55f9713321a --- /dev/null +++ b/tests/Responses/Expected/product_review/index_response.json @@ -0,0 +1,20 @@ +{ + "page": 1, + "limit": 10, + "pages": 1, + "total": 0, + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "first": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "last": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + } + }, + "_embedded": { + "items": [] + } +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json new file mode 100644 index 00000000000..052f37232b0 --- /dev/null +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/show_response.json b/tests/Responses/Expected/product_review/show_response.json new file mode 100644 index 00000000000..15b3378e4e2 --- /dev/null +++ b/tests/Responses/Expected/product_review/show_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} From 0151d20e2abf5154cb70e8f2478c3753e4f0e1e5 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:06:17 +0300 Subject: [PATCH 05/22] [Documentation]: add in toctree and map Product Reviews API --- docs/api/index.rst | 1 + docs/api/map.rst.inc | 1 + docs/api/product_reviews.rst | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index 99e494682a4..ca00abb1d3a 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -21,6 +21,7 @@ The REST API Reference product_attributes product_options product_variants + product_reviews products promotion_coupons promotions diff --git a/docs/api/map.rst.inc b/docs/api/map.rst.inc index 8bd63149971..273f8dd4f51 100644 --- a/docs/api/map.rst.inc +++ b/docs/api/map.rst.inc @@ -15,6 +15,7 @@ * :doc:`/api/product_attributes` * :doc:`/api/product_options` * :doc:`/api/product_variants` +* :doc:`/api/product_reviews` * :doc:`/api/products` * :doc:`/api/promotion_coupons` * :doc:`/api/promotions` diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 77ea810df36..2c5d61ca00e 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -28,7 +28,7 @@ When you get a collection of resources, you will receive objects with the follow .. note:: - Read more about :doc:`ProductReviews docs`. + Read more about :doc:`ProductReviews docs`. Creating a Product Review -------------------------- From 9785c0e5ad815e7d0cd76cbfefb064568e84169e Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 22:24:09 +0300 Subject: [PATCH 06/22] [Documentation]: minor changes --- docs/api/index.rst | 2 +- docs/api/map.rst.inc | 2 +- docs/api/product_reviews.rst | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index ca00abb1d3a..3545bef0017 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -20,8 +20,8 @@ The REST API Reference payments product_attributes product_options - product_variants product_reviews + product_variants products promotion_coupons promotions diff --git a/docs/api/map.rst.inc b/docs/api/map.rst.inc index 273f8dd4f51..d40f5272c2a 100644 --- a/docs/api/map.rst.inc +++ b/docs/api/map.rst.inc @@ -14,8 +14,8 @@ * :doc:`/api/payments` * :doc:`/api/product_attributes` * :doc:`/api/product_options` -* :doc:`/api/product_variants` * :doc:`/api/product_reviews` +* :doc:`/api/product_variants` * :doc:`/api/products` * :doc:`/api/promotion_coupons` * :doc:`/api/promotions` diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 2c5d61ca00e..282729f6b03 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -20,7 +20,7 @@ When you get a collection of resources, you will receive objects with the follow | author | Customer author for product review (This is customer that added the | | | product review; this will contain customer resource information) | +------------------+------------------------------------------------------------------------------------------------+ -| status | Status of product review(New, Accepted, Rejected) | +| status | Status of product review (New, Accepted, Rejected) | +------------------+------------------------------------------------------------------------------------------------+ | reviewSubject | This is the review subject for the product review. For this case of the product review, this | | | will contains a product resource | @@ -53,7 +53,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating(1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ | author | request | Product review author | +---------------+----------------+----------------------------------------------------------+ @@ -117,7 +117,6 @@ Exemplary Response } } - .. warning:: If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. @@ -181,7 +180,6 @@ Exemplary Response } } - Getting a Single Product Review -------------------------------- @@ -438,7 +436,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating(1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ Example From 4aa26f9510a1a27800f768bb831a31ca0f2a0cdf Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 22:34:22 +0300 Subject: [PATCH 07/22] [Product Reviews API] - minor changes --- .../Resources/config/app/config.yml | 2 +- .../Resources/config/routing/main.yml | 8 +- .../Doctrine/ORM/ProductReviewRepository.php | 3 +- .../ProductReviewRepositoryInterface.php | 4 +- tests/Controller/ProductReviewApiTest.php | 24 +-- .../ORM/resources/product_reviews.yml | 3 +- .../product_review/accept_response.json | 166 +++++++++--------- .../change_status_fail_response.json | 2 +- .../product_review/create_response.json | 2 +- .../create_validation_fail_response.json | 2 +- .../product_review/index_response.json | 2 +- .../product_review/reject_response.json | 166 +++++++++--------- 12 files changed, 193 insertions(+), 191 deletions(-) diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml index c3c07d6a1b4..05629014164 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml @@ -7,8 +7,8 @@ imports: - { resource: "@SyliusAdminApiBundle/Resources/config/grids/cart.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/payments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product.yml" } - - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_review.yml" } + - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/promotion.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/shipments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/taxon.yml" } diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml index f7d334ec370..4fcf01b16f5 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml @@ -60,6 +60,10 @@ sylius_api_product_association_type: sylius_api_product_option: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_option.yml" +sylius_api_product_review: + resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" + prefix: /products/{productCode} + sylius_api_product_taxon_position: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_taxon_position.yml" @@ -67,10 +71,6 @@ sylius_api_product_variant: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_variant.yml" prefix: /products/{productCode} -sylius_api_product_review: - resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" - prefix: /products/{productCode} - sylius_api_promotion: resource: "@SyliusAdminApiBundle/Resources/config/routing/promotion.yml" diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 288d76d2447..f29764f4263 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -81,7 +81,8 @@ public function createQueryBuilderByProductCode(string $locale, string $productC /** * {@inheritdoc} */ - public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface { + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface + { return $this->createQueryBuilder('o') ->innerJoin('o.reviewSubject', 'product') ->andWhere('product.code = :productCode') diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index 019ad5dda66..17fec0d3a6a 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -50,10 +50,10 @@ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder; /** - * @param integer $id + * @param mixed $id * @param string $productCode * * @return ReviewInterface|null */ - public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface; + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface; } diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php index 22fb356241b..3e55fab6704 100644 --- a/tests/Controller/ProductReviewApiTest.php +++ b/tests/Controller/ProductReviewApiTest.php @@ -15,7 +15,7 @@ use Lakion\ApiTestCase\JsonApiTestCase; use Sylius\Component\Core\Model\ProductInterface; -use Sylius\Component\Core\Model\ProductReview; +use Sylius\Component\Review\Model\ReviewInterface; use Symfony\Component\HttpFoundation\Response; /** @@ -83,7 +83,7 @@ public function it_allows_showing_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); @@ -183,7 +183,7 @@ public function it_allows_delete_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('DELETE', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, []); @@ -211,7 +211,7 @@ public function it_allows_updating_information_about_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $data = @@ -240,7 +240,7 @@ public function it_allows_updating_partial_information_about_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $data = @@ -267,7 +267,7 @@ public function it_allows_accept_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -287,7 +287,7 @@ public function it_does_not_allows_accept_product_review_if_it_has_not_new_statu /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview2']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -307,7 +307,7 @@ public function it_allows_reject_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/reject', [], [], static::$authorizedHeaderWithAccept); @@ -327,7 +327,7 @@ public function it_does_not_allows_reject_product_review_if_it_has_not_new_statu /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview3']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -341,18 +341,18 @@ public function it_does_not_allows_reject_product_review_if_it_has_not_new_statu * * @return string */ - private function getReviewListUrl(ProductInterface $product) + private function getReviewListUrl(ProductInterface $product): string { return sprintf('/api/v1/products/%s/reviews/', $product->getCode()); } /** * @param ProductInterface $product - * @param ProductReview $productReview + * @param ReviewInterface $productReview * * @return string */ - private function getReviewUrl(ProductInterface $product, ProductReview $productReview) + private function getReviewUrl(ProductInterface $product, ReviewInterface $productReview): string { return sprintf('%s%s', $this->getReviewListUrl($product), $productReview->getId()); } diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml index 6b16363a322..496c4c18f40 100644 --- a/tests/DataFixtures/ORM/resources/product_reviews.yml +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -9,6 +9,7 @@ Sylius\Component\Core\Model\Customer: lastName: King email: example.king@example.com emailCanonical: example.king@example.com + Sylius\Component\Core\Model\Product: product1: fallbackLocale: en_US @@ -39,4 +40,4 @@ Sylius\Component\Core\Model\ProductReview: comment: "mug_review_bad_comment" author: "@customer_example1" status: "accepted" - reviewSubject: "@product1" \ No newline at end of file + reviewSubject: "@product1" diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json index b4765315c14..c2c7e1d51aa 100644 --- a/tests/Responses/Expected/product_review/accept_response.json +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -1,92 +1,92 @@ { - "id": @integer@, - "title": "mug_review_best", - "rating": 4, - "comment": "mug_review_best_comment", - "author": { "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ - } - } - }, - "status": "accepted", - "reviewSubject": { - "id": @integer@, - "code": "MUG_REVIEW_BEST", - "attributes": [], - "options": [], - "associations": [], - "translations": { - "en": { - "locale": "en" - } - }, - "productTaxons": [], - "channels": [], - "reviews": { - "1": { + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { "id": @integer@, - "title": "mug_review_good", - "rating": 3, - "comment": "mug_review_good_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { "self": { - "href": @string@ + "href": @string@ } - } - }, - "status": "rejected", - "createdAt": @string@, - "updatedAt": @string@ - }, - "2": { + } + }, + "status": "accepted", + "reviewSubject": { "id": @integer@, - "title": "mug_review_bad", - "rating": 1, - "comment": "mug_review_bad_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" } - } }, - "status": "accepted", - "createdAt": @string@, - "updatedAt": @string@ - } + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 2.5, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } }, - "averageRating": 2.5, - "images": [], - "_links": { - "self": { - "href": "/api/v1/products/MUG_REVIEW_BEST" - }, - "variants": { - "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" - } - } - }, - "createdAt": @string@, - "updatedAt": @string@ -} \ No newline at end of file + "createdAt": @string@, + "updatedAt": @string@ +} diff --git a/tests/Responses/Expected/product_review/change_status_fail_response.json b/tests/Responses/Expected/product_review/change_status_fail_response.json index f0177c8ed52..bc4332eb237 100644 --- a/tests/Responses/Expected/product_review/change_status_fail_response.json +++ b/tests/Responses/Expected/product_review/change_status_fail_response.json @@ -1,4 +1,4 @@ { "code":400, "message":"" -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json index a545063e07f..4e17382ace6 100644 --- a/tests/Responses/Expected/product_review/create_response.json +++ b/tests/Responses/Expected/product_review/create_response.json @@ -109,4 +109,4 @@ }, "createdAt": @string@, "updatedAt": @string@ -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/create_validation_fail_response.json b/tests/Responses/Expected/product_review/create_validation_fail_response.json index 07e974598a9..d35dab9c82b 100644 --- a/tests/Responses/Expected/product_review/create_validation_fail_response.json +++ b/tests/Responses/Expected/product_review/create_validation_fail_response.json @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/index_response.json b/tests/Responses/Expected/product_review/index_response.json index 55f9713321a..afa682d4ea2 100644 --- a/tests/Responses/Expected/product_review/index_response.json +++ b/tests/Responses/Expected/product_review/index_response.json @@ -17,4 +17,4 @@ "_embedded": { "items": [] } -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json index 052f37232b0..47bbb023f91 100644 --- a/tests/Responses/Expected/product_review/reject_response.json +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -1,92 +1,92 @@ { - "id": @integer@, - "title": "mug_review_best", - "rating": 4, - "comment": "mug_review_best_comment", - "author": { "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ - } - } - }, - "status": "rejected", - "reviewSubject": { - "id": @integer@, - "code": "MUG_REVIEW_BEST", - "attributes": [], - "options": [], - "associations": [], - "translations": { - "en": { - "locale": "en" - } - }, - "productTaxons": [], - "channels": [], - "reviews": { - "1": { + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { "id": @integer@, - "title": "mug_review_good", - "rating": 3, - "comment": "mug_review_good_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { "self": { - "href": @string@ + "href": @string@ } - } - }, - "status": "rejected", - "createdAt": @string@, - "updatedAt": @string@ - }, - "2": { + } + }, + "status": "rejected", + "reviewSubject": { "id": @integer@, - "title": "mug_review_bad", - "rating": 1, - "comment": "mug_review_bad_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" } - } }, - "status": "accepted", - "createdAt": @string@, - "updatedAt": @string@ - } + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } }, - "averageRating": 0, - "images": [], - "_links": { - "self": { - "href": "/api/v1/products/MUG_REVIEW_BEST" - }, - "variants": { - "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" - } - } - }, - "createdAt": @string@, - "updatedAt": @string@ -} \ No newline at end of file + "createdAt": @string@, + "updatedAt": @string@ +} From de75e2710f6fef2fd0aefd67a4255d70641b3624 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 23:32:23 +0300 Subject: [PATCH 08/22] [Documentation]: fix malformed tables Product reviews API --- docs/api/product_reviews.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 282729f6b03..a919ed3a466 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -20,7 +20,7 @@ When you get a collection of resources, you will receive objects with the follow | author | Customer author for product review (This is customer that added the | | | product review; this will contain customer resource information) | +------------------+------------------------------------------------------------------------------------------------+ -| status | Status of product review (New, Accepted, Rejected) | +| status | Status of product review (New, Accepted, Rejected) | +------------------+------------------------------------------------------------------------------------------------+ | reviewSubject | This is the review subject for the product review. For this case of the product review, this | | | will contains a product resource | @@ -53,7 +53,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating (1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ | author | request | Product review author | +---------------+----------------+----------------------------------------------------------+ @@ -436,7 +436,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating (1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ Example From ca4a90a5953c45c8ad98b46e3ff413c0f621f5ea Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 23:33:14 +0300 Subject: [PATCH 09/22] [Product Reviews API] - minor changes for tests --- tests/Controller/ProductReviewApiTest.php | 2 +- tests/DataFixtures/ORM/resources/product_reviews.yml | 5 +++-- .../Responses/Expected/product_review/accept_response.json | 6 +++--- .../Responses/Expected/product_review/create_response.json | 4 ++-- .../Responses/Expected/product_review/reject_response.json | 4 ++-- tests/Responses/Expected/product_review/show_response.json | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php index 3e55fab6704..0892a42c735 100644 --- a/tests/Controller/ProductReviewApiTest.php +++ b/tests/Controller/ProductReviewApiTest.php @@ -288,7 +288,7 @@ public function it_does_not_allows_accept_product_review_if_it_has_not_new_statu $product = $productReviewsData['product1']; /** @var ReviewInterface $productReview */ - $productReview = $productReviewsData['productReview2']; + $productReview = $productReviewsData['productReview3']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); $response = $this->client->getResponse(); diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml index 496c4c18f40..a70d6dd8204 100644 --- a/tests/DataFixtures/ORM/resources/product_reviews.yml +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -26,18 +26,19 @@ Sylius\Component\Core\Model\ProductReview: rating: 4 comment: "mug_review_best_comment" author: "@customer_example1" + status: "new" reviewSubject: "@product1" productReview2: title: mug_review_good rating: 3 comment: "mug_review_good_comment" author: "@customer_example1" - status: "rejected" + status: "new" reviewSubject: "@product1" productReview3: title: mug_review_bad rating: 1 comment: "mug_review_bad_comment" author: "@customer_example1" - status: "accepted" + status: "rejected" reviewSubject: "@product1" diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json index c2c7e1d51aa..c3466545c45 100644 --- a/tests/Responses/Expected/product_review/accept_response.json +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,12 +71,12 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } }, - "averageRating": 2.5, + "averageRating": 4, "images": [], "_links": { "self": { diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json index 4e17382ace6..80421db0cd4 100644 --- a/tests/Responses/Expected/product_review/create_response.json +++ b/tests/Responses/Expected/product_review/create_response.json @@ -69,7 +69,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -91,7 +91,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json index 47bbb023f91..41460f1bbbc 100644 --- a/tests/Responses/Expected/product_review/reject_response.json +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,7 +71,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } diff --git a/tests/Responses/Expected/product_review/show_response.json b/tests/Responses/Expected/product_review/show_response.json index 15b3378e4e2..21ebc78c211 100644 --- a/tests/Responses/Expected/product_review/show_response.json +++ b/tests/Responses/Expected/product_review/show_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,7 +71,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } From 3c1048f9562028ebe0963d8b13c7fd097ed75950 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Wed, 4 Oct 2017 21:53:24 +0300 Subject: [PATCH 10/22] [Product Reviews API] - add API for product reviews --- .../UpdateProductReviewStatusController.php | 106 ++++++++++++++++++ .../Resources/config/app/config.yml | 1 + .../Resources/config/grids/product_review.yml | 46 ++++++++ .../Resources/config/routing/main.yml | 4 + .../config/routing/product_review.yml | 84 ++++++++++++++ .../Resources/config/services/controller.xml | 4 + .../Doctrine/ORM/ProductReviewRepository.php | 32 ++++++ .../ProductReviewRepositoryInterface.php | 18 +++ 8 files changed, 295 insertions(+) create mode 100644 src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml diff --git a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php new file mode 100644 index 00000000000..09dafb218c8 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php @@ -0,0 +1,106 @@ + + */ +final class UpdateProductReviewStatusController +{ + /** + * @var ProductReviewRepositoryInterface + */ + private $productReviewRepository; + + /** + * @var EntityManagerInterface + */ + private $manager; + + /** + * @param RepositoryInterface $productReviewRepository + * @param EntityManagerInterface $manager + */ + public function __construct( + RepositoryInterface $productReviewRepository, + EntityManagerInterface $manager + ) { + $this->productReviewRepository = $productReviewRepository; + $this->manager = $manager; + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function acceptProductReviewAction(Request $request): JsonResponse + { + $reviewId = $request->get('id'); + $productCode = $request->get('productCode'); + + if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** @var ReviewInterface $productReview */ + $productReview = $this->productReviewRepository->findOneByIdAndProductCode( + $reviewId, + $productCode + ); + + $productReview->setStatus(ReviewInterface::STATUS_ACCEPTED); + + $this->manager->persist($productReview); + $this->manager->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** + * @param Request $request + * + * @return JsonResponse + */ + public function rejectProductReviewAction(Request $request): JsonResponse + { + $reviewId = $request->get('id'); + $productCode = $request->get('productCode'); + + if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } + + /** @var ReviewInterface $productReview */ + $productReview = $this->productReviewRepository->findOneByIdAndProductCode( + $reviewId, + $productCode + ); + + $productReview->setStatus(ReviewInterface::STATUS_REJECTED); + + $this->manager->persist($productReview); + $this->manager->flush(); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml index 723b525cfcb..c3c07d6a1b4 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml @@ -8,6 +8,7 @@ imports: - { resource: "@SyliusAdminApiBundle/Resources/config/grids/payments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } + - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_review.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/promotion.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/shipments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/taxon.yml" } diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml new file mode 100644 index 00000000000..36d5f1bd866 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml @@ -0,0 +1,46 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski +# +# @author Paul Stoica + +sylius_grid: + grids: + sylius_admin_api_product_review: + driver: + name: doctrine/orm + options: + class: "%sylius.model.product_review.class%" + repository: + method: createQueryBuilderByProductCode + arguments: ["%locale%", $productCode] + sorting: + date: desc + fields: + date: + type: datetime + label: sylius.ui.date + path: createdAt + sortable: createdAt + options: + format: d-m-Y H:i:s + title: + type: string + label: sylius.ui.title + sortable: ~ + rating: + type: string + label: sylius.ui.rating + sortable: ~ + status: + type: twig + label: sylius.ui.status + reviewSubject: + type: string + label: sylius.ui.product + author: + type: string + label: sylius.ui.customer + filters: + title: + type: string + label: sylius.ui.title diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml index 47b0cb85d8e..f7d334ec370 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml @@ -67,6 +67,10 @@ sylius_api_product_variant: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_variant.yml" prefix: /products/{productCode} +sylius_api_product_review: + resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" + prefix: /products/{productCode} + sylius_api_promotion: resource: "@SyliusAdminApiBundle/Resources/config/routing/promotion.yml" diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml new file mode 100644 index 00000000000..429d3bd9595 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -0,0 +1,84 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski +# +# @author Paul Stoica + +sylius_admin_api_product_review: + resource: | + path: 'reviews' + grid: sylius_admin_api_product_review + alias: sylius.product_review + section: admin_api + only: ['index'] + serialization_version: $version + vars: + route: + parameters: + productCode: $productCode + type: sylius.resource_api + +sylius_admin_api_product_review_create: + path: /reviews/ + methods: [POST] + defaults: + _controller: sylius.controller.product_review:createAction + _sylius: + serialization_groups: [Default, Detailed] + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + factory: + method: createForSubject + arguments: + - expr:notFoundOnNull(service('sylius.repository.product').findOneByCode($productCode)) + +sylius_admin_api_product_review_update: + path: /reviews/{id} + methods: [PUT, PATCH] + defaults: + _controller: sylius.controller.product_review:updateAction + _sylius: + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + +sylius_admin_api_product_review_delete: + path: /reviews/{id} + methods: [DELETE] + defaults: + _controller: sylius.controller.product_review:deleteAction + _sylius: + serialization_version: $version + section: admin_api + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + csrf_protection: false + +sylius_admin_api_product_review_show: + path: /reviews/{code} + methods: [GET] + defaults: + _controller: sylius.controller.product_review:showAction + _sylius: + serialization_version: $version + section: admin_api + serialization_groups: [Default, Detailed] + repository: + method: findOneByIdAndProductCode + arguments: [$code, $productCode] + +sylius_admin_api_product_review_accept: + path: /reviews/{id}/accept + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.update_product_review_status:acceptProductReviewAction + +sylius_admin_api_product_review_reject: + path: /reviews/{id}/reject + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.update_product_review_status:rejectProductReviewAction \ No newline at end of file diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml index 4c21b104799..d6fd87b4d9a 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml @@ -30,5 +30,9 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 3b27935b430..288d76d2447 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -13,6 +13,7 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; +use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductReviewRepositoryInterface; @@ -20,6 +21,7 @@ /** * @author Mateusz Zalewski + * @author Paul Stoica */ class ProductReviewRepository extends EntityRepository implements ProductReviewRepositoryInterface { @@ -60,4 +62,34 @@ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale ->getResult() ; } + + /** + * {@inheritdoc} + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder + { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->innerJoin('product.translations', 'translation') + ->andWhere('translation.locale = :locale') + ->andWhere('product.code = :productCode') + ->setParameter('locale', $locale) + ->setParameter('productCode', $productCode) + ; + } + + /** + * {@inheritdoc} + */ + public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->andWhere('product.code = :productCode') + ->andWhere('o.id = :id') + ->setParameter('productCode', $productCode) + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult() + ; + } } diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index ee68e0c9512..019ad5dda66 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -13,12 +13,14 @@ namespace Sylius\Component\Core\Repository; +use Doctrine\ORM\QueryBuilder; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Review\Model\ReviewInterface; /** * @author Mateusz Zalewski + * @author Paul Stoica */ interface ProductReviewRepositoryInterface extends RepositoryInterface { @@ -38,4 +40,20 @@ public function findLatestByProductId($productId, int $count): array; * @return array|ReviewInterface[] */ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale, ChannelInterface $channel): array; + + /** + * @param string $locale + * @param string $productCode + * + * @return QueryBuilder + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder; + + /** + * @param integer $id + * @param string $productCode + * + * @return ReviewInterface|null + */ + public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface; } From bc07098bcfdb9a9c37fd60fedbd402bd8a097c0b Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Wed, 4 Oct 2017 22:39:50 +0300 Subject: [PATCH 11/22] [Documentation][API] - add product reviews api docs --- docs/api/product_reviews.rst | 637 +++++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 docs/api/product_reviews.rst diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst new file mode 100644 index 00000000000..77ea810df36 --- /dev/null +++ b/docs/api/product_reviews.rst @@ -0,0 +1,637 @@ +Product Reviews API +==================== + +These endpoints will allow you to easily manage product reviews. Base URI is `/api/v1/products/{productCode}/reviews/`. + +Product Reviews API response structure +-------------------------------------- + +When you get a collection of resources, you will receive objects with the following fields: + ++------------------+------------------------------------------------------------------------------------------------+ +| Field | Description | ++==================+================================================================================================+ +| id | Id of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| title | Title of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| comment | Comment of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| author | Customer author for product review (This is customer that added the | +| | product review; this will contain customer resource information) | ++------------------+------------------------------------------------------------------------------------------------+ +| status | Status of product review(New, Accepted, Rejected) | ++------------------+------------------------------------------------------------------------------------------------+ +| reviewSubject | This is the review subject for the product review. For this case of the product review, this | +| | will contains a product resource | ++------------------+------------------------------------------------------------------------------------------------+ + +.. note:: + + Read more about :doc:`ProductReviews docs`. + +Creating a Product Review +-------------------------- + +To create a new product review you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``POST`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/ + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be created | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating(1..5) | ++---------------+----------------+----------------------------------------------------------+ +| author | request | Product review author | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To create new product review for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST \ + --data ' + { + "title": "A product review", + "rating": "3", + "comment": "This is a comment review", + "author": { + "email": "test@example.com" + } + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 201 Created + +.. code-block:: json + + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + + +.. warning:: + + If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. + +Example +^^^^^^^ + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 400 Bad Request + +.. code-block:: json + + { + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } + } + + +Getting a Single Product Review +-------------------------------- + +To retrieve the details of a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To see the details of the product review with ``id = 1``, which is defined for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "id": 1, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + +Collection of Product Reviews +------------------------------ + +To retrieve a paginated list of reviews for a selected product you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/ + ++-------------------------------------+----------------+------------------------------------------------------------+ +| Parameter | Parameter type | Description | ++=====================================+================+============================================================+ +| Authorization | header | Token received during authentication | ++-------------------------------------+----------------+------------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++-------------------------------------+----------------+------------------------------------------------------------+ +| limit | query | *(optional)* Number of items to display per page, | +| | | by default = 10 | ++-------------------------------------+----------------+------------------------------------------------------------+ +| sorting['nameOfField']['direction'] | query | *(optional)* Field and direction of sorting, | +| | | by default 'desc' and 'createdAt' | ++-------------------------------------+----------------+------------------------------------------------------------+ + +Example +^^^^^^^ + +To see the first page of all product reviews for the product with ``code = MUG-TH`` use the method below. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "page": 1, + "limit": 10, + "pages": 1, + "total": 3, + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "first": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "last": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + } + }, + "_embedded": { + "items": [ + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T20:19:06+03:00", + "updatedAt": "2017-10-04T20:19:06+03:00" + }, + { + "id": 3, + "title": "A product review 2", + "rating": 5, + "comment": "This is a comment review 2", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "new", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T18:23:56+03:00", + "updatedAt": "2017-10-04T18:44:08+03:00" + }, + { + "id": 1, + "title": "Test review 3", + "rating": 4, + "comment": "This is a comment review 3", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "accepted", + "reviewSubject": { + "name": "MUG-TH", + "id": 1, + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-03T23:53:24+03:00", + "updatedAt": "2017-10-04T19:18:00+03:00" + } + ] + } + } + +Updating Product Review +------------------------ + +To fully update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PUT`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PUT /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| id | url attribute | Product review id | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating(1..5) | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To fully update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PUT \ + --data ' + { + "title": "A product review", + "rating": "4", + "comment": "This is a comment for review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +To partially update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PATCH /api/v1/products/{productCode}/reviews/{id} + ++------------------------------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++====================================+================+===========================================================+ +| Authorization | header | Token received during authentication | ++------------------------------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++------------------------------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++------------------------------------+----------------+-----------------------------------------------------------+ +| title | request | Product review title | ++------------------------------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To partially update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PATCH \ + --data ' + { + "title": "This is an another title for the review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Deleting a Product Review +-------------------------- + +To delete a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``DELETE`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + DELETE /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be deleted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To delete the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X DELETE + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Accept a Product Review +-------------------------- + +To accept a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/accept`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/accept + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be accepted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To accept the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/accept \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Reject a Product Review +-------------------------- + +To reject a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/reject`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/reject + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be rejected | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To reject the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/reject \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + From 5e41ae6430fc813a6d353a160b39571e550b91b5 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:03:10 +0300 Subject: [PATCH 12/22] [Product Reviews API]: accept/reject endpoints with state machine --- .../UpdateProductReviewStatusController.php | 106 ------------------ .../config/routing/product_review.yml | 12 +- .../Resources/config/services/controller.xml | 4 - 3 files changed, 10 insertions(+), 112 deletions(-) delete mode 100644 src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php diff --git a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php b/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php deleted file mode 100644 index 09dafb218c8..00000000000 --- a/src/Sylius/Bundle/AdminApiBundle/Controller/UpdateProductReviewStatusController.php +++ /dev/null @@ -1,106 +0,0 @@ - - */ -final class UpdateProductReviewStatusController -{ - /** - * @var ProductReviewRepositoryInterface - */ - private $productReviewRepository; - - /** - * @var EntityManagerInterface - */ - private $manager; - - /** - * @param RepositoryInterface $productReviewRepository - * @param EntityManagerInterface $manager - */ - public function __construct( - RepositoryInterface $productReviewRepository, - EntityManagerInterface $manager - ) { - $this->productReviewRepository = $productReviewRepository; - $this->manager = $manager; - } - - /** - * @param Request $request - * - * @return JsonResponse - */ - public function acceptProductReviewAction(Request $request): JsonResponse - { - $reviewId = $request->get('id'); - $productCode = $request->get('productCode'); - - if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** @var ReviewInterface $productReview */ - $productReview = $this->productReviewRepository->findOneByIdAndProductCode( - $reviewId, - $productCode - ); - - $productReview->setStatus(ReviewInterface::STATUS_ACCEPTED); - - $this->manager->persist($productReview); - $this->manager->flush(); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** - * @param Request $request - * - * @return JsonResponse - */ - public function rejectProductReviewAction(Request $request): JsonResponse - { - $reviewId = $request->get('id'); - $productCode = $request->get('productCode'); - - if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && (null === $reviewId || null === $productCode)) { - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } - - /** @var ReviewInterface $productReview */ - $productReview = $this->productReviewRepository->findOneByIdAndProductCode( - $reviewId, - $productCode - ); - - $productReview->setStatus(ReviewInterface::STATUS_REJECTED); - - $this->manager->persist($productReview); - $this->manager->flush(); - - return new JsonResponse(null, Response::HTTP_NO_CONTENT); - } -} diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml index 429d3bd9595..b76b05db185 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -75,10 +75,18 @@ sylius_admin_api_product_review_accept: path: /reviews/{id}/accept methods: [POST, PUT, PATCH] defaults: - _controller: sylius.controller.update_product_review_status:acceptProductReviewAction + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: accept sylius_admin_api_product_review_reject: path: /reviews/{id}/reject methods: [POST, PUT, PATCH] defaults: - _controller: sylius.controller.update_product_review_status:rejectProductReviewAction \ No newline at end of file + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: reject \ No newline at end of file diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml index d6fd87b4d9a..4c21b104799 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/services/controller.xml @@ -30,9 +30,5 @@ - - - - From e998b59df2c0d9d9443a0c00a3627a7fb22ff779 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:04:03 +0300 Subject: [PATCH 13/22] [Unit Testing]: ProductReviewApi Controller --- tests/Controller/ProductReviewApiTest.php | 359 ++++++++++++++++++ .../ORM/resources/product_reviews.yml | 42 ++ .../product_review/accept_response.json | 92 +++++ .../change_status_fail_response.json | 4 + .../product_review/create_response.json | 112 ++++++ .../create_validation_fail_response.json | 39 ++ .../product_review/index_response.json | 20 + .../product_review/reject_response.json | 92 +++++ .../product_review/show_response.json | 92 +++++ 9 files changed, 852 insertions(+) create mode 100644 tests/Controller/ProductReviewApiTest.php create mode 100644 tests/DataFixtures/ORM/resources/product_reviews.yml create mode 100644 tests/Responses/Expected/product_review/accept_response.json create mode 100644 tests/Responses/Expected/product_review/change_status_fail_response.json create mode 100644 tests/Responses/Expected/product_review/create_response.json create mode 100644 tests/Responses/Expected/product_review/create_validation_fail_response.json create mode 100644 tests/Responses/Expected/product_review/index_response.json create mode 100644 tests/Responses/Expected/product_review/reject_response.json create mode 100644 tests/Responses/Expected/product_review/show_response.json diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php new file mode 100644 index 00000000000..22fb356241b --- /dev/null +++ b/tests/Controller/ProductReviewApiTest.php @@ -0,0 +1,359 @@ + + */ +final class ProductReviewApiTest extends JsonApiTestCase +{ + /** + * @var array + */ + private static $authorizedHeaderWithContentType = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'CONTENT_TYPE' => 'application/json', + ]; + + /** + * @var array + */ + private static $authorizedHeaderWithAccept = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'ACCEPT' => 'application/json', + ]; + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_list_when_access_is_denied() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product)); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'authentication/access_denied_response', Response::HTTP_UNAUTHORIZED); + } + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_when_it_does_not_exist() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_showing_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/show_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_indexing_product_reviews() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/index_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_create_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $data = +<<client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_response', Response::HTTP_CREATED); + } + + /** + * @test + */ + public function it_does_not_allow_to_create_product_review_without_required_fields() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, []); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_validation_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_does_not_allow_delete_product_review_if_it_does_not_exist() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('DELETE', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_delete_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('DELETE', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, []); + + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_updating_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PUT', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_updating_partial_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('resources/locales.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PATCH', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_accept_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/accept_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_accept_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview2']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_allows_reject_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/reject', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/reject_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_reject_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ProductReview $productReview */ + $productReview = $productReviewsData['productReview3']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @param ProductInterface $product + * + * @return string + */ + private function getReviewListUrl(ProductInterface $product) + { + return sprintf('/api/v1/products/%s/reviews/', $product->getCode()); + } + + /** + * @param ProductInterface $product + * @param ProductReview $productReview + * + * @return string + */ + private function getReviewUrl(ProductInterface $product, ProductReview $productReview) + { + return sprintf('%s%s', $this->getReviewListUrl($product), $productReview->getId()); + } +} diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml new file mode 100644 index 00000000000..6b16363a322 --- /dev/null +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -0,0 +1,42 @@ +Sylius\Component\Core\Model\Customer: + customer_example1: + firstName: Example + lastName: Queen + email: example.queen@example.com + emailCanonical: example.queen@example.com + customer_example2: + firstName: Example + lastName: King + email: example.king@example.com + emailCanonical: example.king@example.com +Sylius\Component\Core\Model\Product: + product1: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_BEST + product2: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_GOOD + +Sylius\Component\Core\Model\ProductReview: + productReview1: + title: mug_review_best + rating: 4 + comment: "mug_review_best_comment" + author: "@customer_example1" + reviewSubject: "@product1" + productReview2: + title: mug_review_good + rating: 3 + comment: "mug_review_good_comment" + author: "@customer_example1" + status: "rejected" + reviewSubject: "@product1" + productReview3: + title: mug_review_bad + rating: 1 + comment: "mug_review_bad_comment" + author: "@customer_example1" + status: "accepted" + reviewSubject: "@product1" \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json new file mode 100644 index 00000000000..b4765315c14 --- /dev/null +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 2.5, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/change_status_fail_response.json b/tests/Responses/Expected/product_review/change_status_fail_response.json new file mode 100644 index 00000000000..f0177c8ed52 --- /dev/null +++ b/tests/Responses/Expected/product_review/change_status_fail_response.json @@ -0,0 +1,4 @@ +{ + "code":400, + "message":"" +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json new file mode 100644 index 00000000000..a545063e07f --- /dev/null +++ b/tests/Responses/Expected/product_review/create_response.json @@ -0,0 +1,112 @@ +{ + "id": @integer@, + "title": "J_REVIEW", + "rating": 3, + "comment": "J_REVIEW_COMMENT", + "author": { + "id": @integer@, + "email": "j@example.com", + "emailCanonical": "j@example.com", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": [ + { + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + ], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/create_validation_fail_response.json b/tests/Responses/Expected/product_review/create_validation_fail_response.json new file mode 100644 index 00000000000..07e974598a9 --- /dev/null +++ b/tests/Responses/Expected/product_review/create_validation_fail_response.json @@ -0,0 +1,39 @@ +{ + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/index_response.json b/tests/Responses/Expected/product_review/index_response.json new file mode 100644 index 00000000000..55f9713321a --- /dev/null +++ b/tests/Responses/Expected/product_review/index_response.json @@ -0,0 +1,20 @@ +{ + "page": 1, + "limit": 10, + "pages": 1, + "total": 0, + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "first": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "last": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + } + }, + "_embedded": { + "items": [] + } +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json new file mode 100644 index 00000000000..052f37232b0 --- /dev/null +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} \ No newline at end of file diff --git a/tests/Responses/Expected/product_review/show_response.json b/tests/Responses/Expected/product_review/show_response.json new file mode 100644 index 00000000000..15b3378e4e2 --- /dev/null +++ b/tests/Responses/Expected/product_review/show_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} From dd37af34c49c56da2862f0a672a51a4245969736 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 6 Oct 2017 00:06:17 +0300 Subject: [PATCH 14/22] [Documentation]: add in toctree and map Product Reviews API --- docs/api/index.rst | 1 + docs/api/map.rst.inc | 1 + docs/api/product_reviews.rst | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index 99e494682a4..ca00abb1d3a 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -21,6 +21,7 @@ The REST API Reference product_attributes product_options product_variants + product_reviews products promotion_coupons promotions diff --git a/docs/api/map.rst.inc b/docs/api/map.rst.inc index 8bd63149971..273f8dd4f51 100644 --- a/docs/api/map.rst.inc +++ b/docs/api/map.rst.inc @@ -15,6 +15,7 @@ * :doc:`/api/product_attributes` * :doc:`/api/product_options` * :doc:`/api/product_variants` +* :doc:`/api/product_reviews` * :doc:`/api/products` * :doc:`/api/promotion_coupons` * :doc:`/api/promotions` diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 77ea810df36..2c5d61ca00e 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -28,7 +28,7 @@ When you get a collection of resources, you will receive objects with the follow .. note:: - Read more about :doc:`ProductReviews docs`. + Read more about :doc:`ProductReviews docs`. Creating a Product Review -------------------------- From 02286020796fc66fa1c6c8cd7f2f17b41cd9e505 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 22:24:09 +0300 Subject: [PATCH 15/22] [Documentation]: minor changes --- docs/api/index.rst | 2 +- docs/api/map.rst.inc | 2 +- docs/api/product_reviews.rst | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/api/index.rst b/docs/api/index.rst index ca00abb1d3a..3545bef0017 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -20,8 +20,8 @@ The REST API Reference payments product_attributes product_options - product_variants product_reviews + product_variants products promotion_coupons promotions diff --git a/docs/api/map.rst.inc b/docs/api/map.rst.inc index 273f8dd4f51..d40f5272c2a 100644 --- a/docs/api/map.rst.inc +++ b/docs/api/map.rst.inc @@ -14,8 +14,8 @@ * :doc:`/api/payments` * :doc:`/api/product_attributes` * :doc:`/api/product_options` -* :doc:`/api/product_variants` * :doc:`/api/product_reviews` +* :doc:`/api/product_variants` * :doc:`/api/products` * :doc:`/api/promotion_coupons` * :doc:`/api/promotions` diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 2c5d61ca00e..282729f6b03 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -20,7 +20,7 @@ When you get a collection of resources, you will receive objects with the follow | author | Customer author for product review (This is customer that added the | | | product review; this will contain customer resource information) | +------------------+------------------------------------------------------------------------------------------------+ -| status | Status of product review(New, Accepted, Rejected) | +| status | Status of product review (New, Accepted, Rejected) | +------------------+------------------------------------------------------------------------------------------------+ | reviewSubject | This is the review subject for the product review. For this case of the product review, this | | | will contains a product resource | @@ -53,7 +53,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating(1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ | author | request | Product review author | +---------------+----------------+----------------------------------------------------------+ @@ -117,7 +117,6 @@ Exemplary Response } } - .. warning:: If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. @@ -181,7 +180,6 @@ Exemplary Response } } - Getting a Single Product Review -------------------------------- @@ -438,7 +436,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating(1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ Example From d571b9c88a0a74226c71acc19706e6698ab0e300 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 22:34:22 +0300 Subject: [PATCH 16/22] [Product Reviews API] - minor changes --- .../Resources/config/app/config.yml | 2 +- .../Resources/config/routing/main.yml | 8 +- .../Doctrine/ORM/ProductReviewRepository.php | 3 +- .../ProductReviewRepositoryInterface.php | 4 +- tests/Controller/ProductReviewApiTest.php | 24 +-- .../ORM/resources/product_reviews.yml | 3 +- .../product_review/accept_response.json | 166 +++++++++--------- .../change_status_fail_response.json | 2 +- .../product_review/create_response.json | 2 +- .../create_validation_fail_response.json | 2 +- .../product_review/index_response.json | 2 +- .../product_review/reject_response.json | 166 +++++++++--------- 12 files changed, 193 insertions(+), 191 deletions(-) diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml index c3c07d6a1b4..05629014164 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml @@ -7,8 +7,8 @@ imports: - { resource: "@SyliusAdminApiBundle/Resources/config/grids/cart.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/payments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product.yml" } - - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_review.yml" } + - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/promotion.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/shipments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/taxon.yml" } diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml index f7d334ec370..4fcf01b16f5 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml @@ -60,6 +60,10 @@ sylius_api_product_association_type: sylius_api_product_option: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_option.yml" +sylius_api_product_review: + resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" + prefix: /products/{productCode} + sylius_api_product_taxon_position: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_taxon_position.yml" @@ -67,10 +71,6 @@ sylius_api_product_variant: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_variant.yml" prefix: /products/{productCode} -sylius_api_product_review: - resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" - prefix: /products/{productCode} - sylius_api_promotion: resource: "@SyliusAdminApiBundle/Resources/config/routing/promotion.yml" diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 288d76d2447..f29764f4263 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -81,7 +81,8 @@ public function createQueryBuilderByProductCode(string $locale, string $productC /** * {@inheritdoc} */ - public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface { + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface + { return $this->createQueryBuilder('o') ->innerJoin('o.reviewSubject', 'product') ->andWhere('product.code = :productCode') diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index 019ad5dda66..17fec0d3a6a 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -50,10 +50,10 @@ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder; /** - * @param integer $id + * @param mixed $id * @param string $productCode * * @return ReviewInterface|null */ - public function findOneByIdAndProductCode(string $id, string $productCode): ?ReviewInterface; + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface; } diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php index 22fb356241b..3e55fab6704 100644 --- a/tests/Controller/ProductReviewApiTest.php +++ b/tests/Controller/ProductReviewApiTest.php @@ -15,7 +15,7 @@ use Lakion\ApiTestCase\JsonApiTestCase; use Sylius\Component\Core\Model\ProductInterface; -use Sylius\Component\Core\Model\ProductReview; +use Sylius\Component\Review\Model\ReviewInterface; use Symfony\Component\HttpFoundation\Response; /** @@ -83,7 +83,7 @@ public function it_allows_showing_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); @@ -183,7 +183,7 @@ public function it_allows_delete_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('DELETE', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, []); @@ -211,7 +211,7 @@ public function it_allows_updating_information_about_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $data = @@ -240,7 +240,7 @@ public function it_allows_updating_partial_information_about_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $data = @@ -267,7 +267,7 @@ public function it_allows_accept_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -287,7 +287,7 @@ public function it_does_not_allows_accept_product_review_if_it_has_not_new_statu /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview2']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -307,7 +307,7 @@ public function it_allows_reject_product_review() /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview1']; $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/reject', [], [], static::$authorizedHeaderWithAccept); @@ -327,7 +327,7 @@ public function it_does_not_allows_reject_product_review_if_it_has_not_new_statu /** @var ProductInterface $product */ $product = $productReviewsData['product1']; - /** @var ProductReview $productReview */ + /** @var ReviewInterface $productReview */ $productReview = $productReviewsData['productReview3']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); @@ -341,18 +341,18 @@ public function it_does_not_allows_reject_product_review_if_it_has_not_new_statu * * @return string */ - private function getReviewListUrl(ProductInterface $product) + private function getReviewListUrl(ProductInterface $product): string { return sprintf('/api/v1/products/%s/reviews/', $product->getCode()); } /** * @param ProductInterface $product - * @param ProductReview $productReview + * @param ReviewInterface $productReview * * @return string */ - private function getReviewUrl(ProductInterface $product, ProductReview $productReview) + private function getReviewUrl(ProductInterface $product, ReviewInterface $productReview): string { return sprintf('%s%s', $this->getReviewListUrl($product), $productReview->getId()); } diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml index 6b16363a322..496c4c18f40 100644 --- a/tests/DataFixtures/ORM/resources/product_reviews.yml +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -9,6 +9,7 @@ Sylius\Component\Core\Model\Customer: lastName: King email: example.king@example.com emailCanonical: example.king@example.com + Sylius\Component\Core\Model\Product: product1: fallbackLocale: en_US @@ -39,4 +40,4 @@ Sylius\Component\Core\Model\ProductReview: comment: "mug_review_bad_comment" author: "@customer_example1" status: "accepted" - reviewSubject: "@product1" \ No newline at end of file + reviewSubject: "@product1" diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json index b4765315c14..c2c7e1d51aa 100644 --- a/tests/Responses/Expected/product_review/accept_response.json +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -1,92 +1,92 @@ { - "id": @integer@, - "title": "mug_review_best", - "rating": 4, - "comment": "mug_review_best_comment", - "author": { "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ - } - } - }, - "status": "accepted", - "reviewSubject": { - "id": @integer@, - "code": "MUG_REVIEW_BEST", - "attributes": [], - "options": [], - "associations": [], - "translations": { - "en": { - "locale": "en" - } - }, - "productTaxons": [], - "channels": [], - "reviews": { - "1": { + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { "id": @integer@, - "title": "mug_review_good", - "rating": 3, - "comment": "mug_review_good_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { "self": { - "href": @string@ + "href": @string@ } - } - }, - "status": "rejected", - "createdAt": @string@, - "updatedAt": @string@ - }, - "2": { + } + }, + "status": "accepted", + "reviewSubject": { "id": @integer@, - "title": "mug_review_bad", - "rating": 1, - "comment": "mug_review_bad_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" } - } }, - "status": "accepted", - "createdAt": @string@, - "updatedAt": @string@ - } + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 2.5, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } }, - "averageRating": 2.5, - "images": [], - "_links": { - "self": { - "href": "/api/v1/products/MUG_REVIEW_BEST" - }, - "variants": { - "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" - } - } - }, - "createdAt": @string@, - "updatedAt": @string@ -} \ No newline at end of file + "createdAt": @string@, + "updatedAt": @string@ +} diff --git a/tests/Responses/Expected/product_review/change_status_fail_response.json b/tests/Responses/Expected/product_review/change_status_fail_response.json index f0177c8ed52..bc4332eb237 100644 --- a/tests/Responses/Expected/product_review/change_status_fail_response.json +++ b/tests/Responses/Expected/product_review/change_status_fail_response.json @@ -1,4 +1,4 @@ { "code":400, "message":"" -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json index a545063e07f..4e17382ace6 100644 --- a/tests/Responses/Expected/product_review/create_response.json +++ b/tests/Responses/Expected/product_review/create_response.json @@ -109,4 +109,4 @@ }, "createdAt": @string@, "updatedAt": @string@ -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/create_validation_fail_response.json b/tests/Responses/Expected/product_review/create_validation_fail_response.json index 07e974598a9..d35dab9c82b 100644 --- a/tests/Responses/Expected/product_review/create_validation_fail_response.json +++ b/tests/Responses/Expected/product_review/create_validation_fail_response.json @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/index_response.json b/tests/Responses/Expected/product_review/index_response.json index 55f9713321a..afa682d4ea2 100644 --- a/tests/Responses/Expected/product_review/index_response.json +++ b/tests/Responses/Expected/product_review/index_response.json @@ -17,4 +17,4 @@ "_embedded": { "items": [] } -} \ No newline at end of file +} diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json index 052f37232b0..47bbb023f91 100644 --- a/tests/Responses/Expected/product_review/reject_response.json +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -1,92 +1,92 @@ { - "id": @integer@, - "title": "mug_review_best", - "rating": 4, - "comment": "mug_review_best_comment", - "author": { "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ - } - } - }, - "status": "rejected", - "reviewSubject": { - "id": @integer@, - "code": "MUG_REVIEW_BEST", - "attributes": [], - "options": [], - "associations": [], - "translations": { - "en": { - "locale": "en" - } - }, - "productTaxons": [], - "channels": [], - "reviews": { - "1": { + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { "id": @integer@, - "title": "mug_review_good", - "rating": 3, - "comment": "mug_review_good_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { "self": { - "href": @string@ + "href": @string@ } - } - }, - "status": "rejected", - "createdAt": @string@, - "updatedAt": @string@ - }, - "2": { + } + }, + "status": "rejected", + "reviewSubject": { "id": @integer@, - "title": "mug_review_bad", - "rating": 1, - "comment": "mug_review_bad_comment", - "author": { - "id": @integer@, - "email": "example.queen@example.com", - "emailCanonical": "example.queen@example.com", - "firstName": "Example", - "lastName": "Queen", - "gender": "u", - "_links": { - "self": { - "href": @string@ + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" } - } }, - "status": "accepted", - "createdAt": @string@, - "updatedAt": @string@ - } + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } }, - "averageRating": 0, - "images": [], - "_links": { - "self": { - "href": "/api/v1/products/MUG_REVIEW_BEST" - }, - "variants": { - "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" - } - } - }, - "createdAt": @string@, - "updatedAt": @string@ -} \ No newline at end of file + "createdAt": @string@, + "updatedAt": @string@ +} From 3933ed23cc8cec7b6ff5f7d332c7d942e639b215 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 23:32:23 +0300 Subject: [PATCH 17/22] [Documentation]: fix malformed tables Product reviews API --- docs/api/product_reviews.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index 282729f6b03..a919ed3a466 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -20,7 +20,7 @@ When you get a collection of resources, you will receive objects with the follow | author | Customer author for product review (This is customer that added the | | | product review; this will contain customer resource information) | +------------------+------------------------------------------------------------------------------------------------+ -| status | Status of product review (New, Accepted, Rejected) | +| status | Status of product review (New, Accepted, Rejected) | +------------------+------------------------------------------------------------------------------------------------+ | reviewSubject | This is the review subject for the product review. For this case of the product review, this | | | will contains a product resource | @@ -53,7 +53,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating (1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ | author | request | Product review author | +---------------+----------------+----------------------------------------------------------+ @@ -436,7 +436,7 @@ Definition +---------------+----------------+----------------------------------------------------------+ | comment | request | Product review comment | +---------------+----------------+----------------------------------------------------------+ -| rating | request | Product review rating (1..5) | +| rating | request | Product review rating (1..5) | +---------------+----------------+----------------------------------------------------------+ Example From 3cf247a960146d3daf88bfef40ff4b6973d3dc65 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Mon, 9 Oct 2017 23:33:14 +0300 Subject: [PATCH 18/22] [Product Reviews API] - minor changes for tests --- tests/Controller/ProductReviewApiTest.php | 2 +- tests/DataFixtures/ORM/resources/product_reviews.yml | 5 +++-- .../Responses/Expected/product_review/accept_response.json | 6 +++--- .../Responses/Expected/product_review/create_response.json | 4 ++-- .../Responses/Expected/product_review/reject_response.json | 4 ++-- tests/Responses/Expected/product_review/show_response.json | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php index 3e55fab6704..0892a42c735 100644 --- a/tests/Controller/ProductReviewApiTest.php +++ b/tests/Controller/ProductReviewApiTest.php @@ -288,7 +288,7 @@ public function it_does_not_allows_accept_product_review_if_it_has_not_new_statu $product = $productReviewsData['product1']; /** @var ReviewInterface $productReview */ - $productReview = $productReviewsData['productReview2']; + $productReview = $productReviewsData['productReview3']; $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); $response = $this->client->getResponse(); diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml index 496c4c18f40..a70d6dd8204 100644 --- a/tests/DataFixtures/ORM/resources/product_reviews.yml +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -26,18 +26,19 @@ Sylius\Component\Core\Model\ProductReview: rating: 4 comment: "mug_review_best_comment" author: "@customer_example1" + status: "new" reviewSubject: "@product1" productReview2: title: mug_review_good rating: 3 comment: "mug_review_good_comment" author: "@customer_example1" - status: "rejected" + status: "new" reviewSubject: "@product1" productReview3: title: mug_review_bad rating: 1 comment: "mug_review_bad_comment" author: "@customer_example1" - status: "accepted" + status: "rejected" reviewSubject: "@product1" diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json index c2c7e1d51aa..c3466545c45 100644 --- a/tests/Responses/Expected/product_review/accept_response.json +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,12 +71,12 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } }, - "averageRating": 2.5, + "averageRating": 4, "images": [], "_links": { "self": { diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json index 4e17382ace6..80421db0cd4 100644 --- a/tests/Responses/Expected/product_review/create_response.json +++ b/tests/Responses/Expected/product_review/create_response.json @@ -69,7 +69,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -91,7 +91,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json index 47bbb023f91..41460f1bbbc 100644 --- a/tests/Responses/Expected/product_review/reject_response.json +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,7 +71,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } diff --git a/tests/Responses/Expected/product_review/show_response.json b/tests/Responses/Expected/product_review/show_response.json index 15b3378e4e2..21ebc78c211 100644 --- a/tests/Responses/Expected/product_review/show_response.json +++ b/tests/Responses/Expected/product_review/show_response.json @@ -49,7 +49,7 @@ } } }, - "status": "rejected", + "status": "new", "createdAt": @string@, "updatedAt": @string@ }, @@ -71,7 +71,7 @@ } } }, - "status": "accepted", + "status": "rejected", "createdAt": @string@, "updatedAt": @string@ } From 859021c606fd0d0f75ec4c394c8649f8ddb1c146 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 17 Nov 2017 21:13:29 +0200 Subject: [PATCH 19/22] [Documentation]: texts improvments Product reviews API --- docs/api/product_reviews.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst index a919ed3a466..e3999e4bbf9 100644 --- a/docs/api/product_reviews.rst +++ b/docs/api/product_reviews.rst @@ -18,12 +18,12 @@ When you get a collection of resources, you will receive objects with the follow | comment | Comment of product review | +------------------+------------------------------------------------------------------------------------------------+ | author | Customer author for product review (This is customer that added the | -| | product review; this will contain customer resource information) | +| | product review; this will contain customer resource information) | +------------------+------------------------------------------------------------------------------------------------+ | status | Status of product review (New, Accepted, Rejected) | +------------------+------------------------------------------------------------------------------------------------+ | reviewSubject | This is the review subject for the product review. For this case of the product review, this | -| | will contains a product resource | +| | will contain a product resource | +------------------+------------------------------------------------------------------------------------------------+ .. note:: @@ -61,7 +61,7 @@ Definition Example ^^^^^^^ -To create new product review for the product with ``code = MUG-TH`` use the below method. +To create a new product review for the product with ``code = MUG-TH`` use the below method. .. code-block:: bash @@ -107,8 +107,8 @@ Exemplary Response }, "status": "new", "reviewSubject": { - "name": "MUG-TH", "id": 1, + "name": "MUG-TH", "code": "MUG-TH", "attributes": [], "options": [], @@ -116,7 +116,7 @@ Exemplary Response "translations": [] } } - + .. warning:: If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. @@ -240,8 +240,8 @@ Exemplary Response }, "status": "new", "reviewSubject": { - "name": "MUG-TH", "id": 1, + "name": "MUG-TH", "code": "MUG-TH", "attributes": [], "options": [], @@ -330,8 +330,8 @@ Exemplary Response }, "status": "new", "reviewSubject": { - "name": "MUG-TH", "id": 1, + "name": "MUG-TH", "code": "MUG-TH", "options": [], "averageRating": 0, @@ -361,8 +361,8 @@ Exemplary Response }, "status": "new", "reviewSubject": { - "name": "MUG-TH", "id": 1, + "name": "MUG-TH", "code": "MUG-TH", "options": [], "averageRating": 0, @@ -392,8 +392,8 @@ Exemplary Response }, "status": "accepted", "reviewSubject": { - "name": "MUG-TH", "id": 1, + "name": "MUG-TH", "code": "MUG-TH", "options": [], "averageRating": 0, From ca795cb4059577fd20ff99b625aa649a64fef8e4 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Fri, 17 Nov 2017 21:25:05 +0200 Subject: [PATCH 20/22] Product review API syntax improvments --- .../AdminApiBundle/Resources/config/grids/product_review.yml | 2 -- .../Resources/config/routing/product_review.yml | 4 +--- .../CoreBundle/Doctrine/ORM/ProductReviewRepository.php | 1 - .../Core/Repository/ProductReviewRepositoryInterface.php | 3 +-- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml index 36d5f1bd866..ed74f66bc14 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml @@ -1,7 +1,5 @@ # This file is part of the Sylius package. # (c) Paweł Jędrzejewski -# -# @author Paul Stoica sylius_grid: grids: diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml index b76b05db185..eab9184d239 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -1,7 +1,5 @@ # This file is part of the Sylius package. # (c) Paweł Jędrzejewski -# -# @author Paul Stoica sylius_admin_api_product_review: resource: | @@ -89,4 +87,4 @@ sylius_admin_api_product_review_reject: _sylius: state_machine: graph: sylius_product_review - transition: reject \ No newline at end of file + transition: reject diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index f29764f4263..dd6a8333157 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -21,7 +21,6 @@ /** * @author Mateusz Zalewski - * @author Paul Stoica */ class ProductReviewRepository extends EntityRepository implements ProductReviewRepositoryInterface { diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index 17fec0d3a6a..8da1f32f05f 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -20,7 +20,6 @@ /** * @author Mateusz Zalewski - * @author Paul Stoica */ interface ProductReviewRepositoryInterface extends RepositoryInterface { @@ -51,7 +50,7 @@ public function createQueryBuilderByProductCode(string $locale, string $productC /** * @param mixed $id - * @param string $productCode + * @param string $productCode * * @return ReviewInterface|null */ From aca53a4b113c9c157edf36fb3816243ab7ceee77 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Wed, 4 Oct 2017 21:53:24 +0300 Subject: [PATCH 21/22] [Product Reviews API] - add API for product reviews --- docs/api/index.rst | 2 + docs/api/map.rst.inc | 2 + docs/api/product_reviews.rst | 635 ++++++++++++++++++ .../Resources/config/app/config.yml | 1 + .../Resources/config/grids/product_review.yml | 44 ++ .../Resources/config/routing/main.yml | 4 + .../config/routing/product_review.yml | 90 +++ .../Doctrine/ORM/ProductReviewRepository.php | 32 + .../ProductReviewRepositoryInterface.php | 17 + tests/Controller/ProductReviewApiTest.php | 359 ++++++++++ .../ORM/resources/product_reviews.yml | 44 ++ .../product_review/accept_response.json | 92 +++ .../change_status_fail_response.json | 4 + .../product_review/create_response.json | 112 +++ .../create_validation_fail_response.json | 39 ++ .../product_review/index_response.json | 20 + .../product_review/reject_response.json | 92 +++ .../product_review/show_response.json | 92 +++ 18 files changed, 1681 insertions(+) create mode 100644 docs/api/product_reviews.rst create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml create mode 100644 src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml create mode 100644 tests/Controller/ProductReviewApiTest.php create mode 100644 tests/DataFixtures/ORM/resources/product_reviews.yml create mode 100644 tests/Responses/Expected/product_review/accept_response.json create mode 100644 tests/Responses/Expected/product_review/change_status_fail_response.json create mode 100644 tests/Responses/Expected/product_review/create_response.json create mode 100644 tests/Responses/Expected/product_review/create_validation_fail_response.json create mode 100644 tests/Responses/Expected/product_review/index_response.json create mode 100644 tests/Responses/Expected/product_review/reject_response.json create mode 100644 tests/Responses/Expected/product_review/show_response.json diff --git a/docs/api/index.rst b/docs/api/index.rst index 99e494682a4..64974b5d4c2 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -20,7 +20,9 @@ The REST API Reference payments product_attributes product_options + product_reviews product_variants + product_reviews products promotion_coupons promotions diff --git a/docs/api/map.rst.inc b/docs/api/map.rst.inc index 8bd63149971..c67c48f078f 100644 --- a/docs/api/map.rst.inc +++ b/docs/api/map.rst.inc @@ -14,7 +14,9 @@ * :doc:`/api/payments` * :doc:`/api/product_attributes` * :doc:`/api/product_options` +* :doc:`/api/product_reviews` * :doc:`/api/product_variants` +* :doc:`/api/product_reviews` * :doc:`/api/products` * :doc:`/api/promotion_coupons` * :doc:`/api/promotions` diff --git a/docs/api/product_reviews.rst b/docs/api/product_reviews.rst new file mode 100644 index 00000000000..e3999e4bbf9 --- /dev/null +++ b/docs/api/product_reviews.rst @@ -0,0 +1,635 @@ +Product Reviews API +==================== + +These endpoints will allow you to easily manage product reviews. Base URI is `/api/v1/products/{productCode}/reviews/`. + +Product Reviews API response structure +-------------------------------------- + +When you get a collection of resources, you will receive objects with the following fields: + ++------------------+------------------------------------------------------------------------------------------------+ +| Field | Description | ++==================+================================================================================================+ +| id | Id of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| title | Title of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| comment | Comment of product review | ++------------------+------------------------------------------------------------------------------------------------+ +| author | Customer author for product review (This is customer that added the | +| | product review; this will contain customer resource information) | ++------------------+------------------------------------------------------------------------------------------------+ +| status | Status of product review (New, Accepted, Rejected) | ++------------------+------------------------------------------------------------------------------------------------+ +| reviewSubject | This is the review subject for the product review. For this case of the product review, this | +| | will contain a product resource | ++------------------+------------------------------------------------------------------------------------------------+ + +.. note:: + + Read more about :doc:`ProductReviews docs`. + +Creating a Product Review +-------------------------- + +To create a new product review you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``POST`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/ + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be created | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating (1..5) | ++---------------+----------------+----------------------------------------------------------+ +| author | request | Product review author | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To create a new product review for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST \ + --data ' + { + "title": "A product review", + "rating": "3", + "comment": "This is a comment review", + "author": { + "email": "test@example.com" + } + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 201 Created + +.. code-block:: json + + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "id": 1, + "name": "MUG-TH", + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + +.. warning:: + + If you try to create a resource without title, rating, comment or author, you will receive a ``400 Bad Request`` error. + +Example +^^^^^^^ + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 400 Bad Request + +.. code-block:: json + + { + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } + } + +Getting a Single Product Review +-------------------------------- + +To retrieve the details of a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To see the details of the product review with ``id = 1``, which is defined for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "id": 1, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "emailCanonical": "test@example.com", + "gender": "u", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "id": 1, + "name": "MUG-TH", + "code": "MUG-TH", + "attributes": [], + "options": [], + "associations": [], + "translations": [] + } + } + +Collection of Product Reviews +------------------------------ + +To retrieve a paginated list of reviews for a selected product you will need to call the ``/api/v1/products/{productCode}/reviews/`` endpoint with the ``GET`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/products/{productCode}/reviews/ + ++-------------------------------------+----------------+------------------------------------------------------------+ +| Parameter | Parameter type | Description | ++=====================================+================+============================================================+ +| Authorization | header | Token received during authentication | ++-------------------------------------+----------------+------------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be displayed | ++-------------------------------------+----------------+------------------------------------------------------------+ +| limit | query | *(optional)* Number of items to display per page, | +| | | by default = 10 | ++-------------------------------------+----------------+------------------------------------------------------------+ +| sorting['nameOfField']['direction'] | query | *(optional)* Field and direction of sorting, | +| | | by default 'desc' and 'createdAt' | ++-------------------------------------+----------------+------------------------------------------------------------+ + +Example +^^^^^^^ + +To see the first page of all product reviews for the product with ``code = MUG-TH`` use the method below. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/ \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 200 OK + +.. code-block:: json + + { + "page": 1, + "limit": 10, + "pages": 1, + "total": 3, + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "first": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + }, + "last": { + "href": "/api/v1/products/MUG-TH/reviews/?page=1&limit=10" + } + }, + "_embedded": { + "items": [ + { + "id": 4, + "title": "A product review", + "rating": 3, + "comment": "This is a comment review", + "author": { + "id": 2, + "email": "test@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/2" + } + } + }, + "status": "new", + "reviewSubject": { + "id": 1, + "name": "MUG-TH", + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T20:19:06+03:00", + "updatedAt": "2017-10-04T20:19:06+03:00" + }, + { + "id": 3, + "title": "A product review 2", + "rating": 5, + "comment": "This is a comment review 2", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "new", + "reviewSubject": { + "id": 1, + "name": "MUG-TH", + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-04T18:23:56+03:00", + "updatedAt": "2017-10-04T18:44:08+03:00" + }, + { + "id": 1, + "title": "Test review 3", + "rating": 4, + "comment": "This is a comment review 3", + "author": { + "id": 1, + "email": "onetest@example.com", + "_links": { + "self": { + "href": "/api/v1/customers/1" + } + } + }, + "status": "accepted", + "reviewSubject": { + "id": 1, + "name": "MUG-TH", + "code": "MUG-TH", + "options": [], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG-TH" + } + } + }, + "createdAt": "2017-10-03T23:53:24+03:00", + "updatedAt": "2017-10-04T19:18:00+03:00" + } + ] + } + } + +Updating Product Review +------------------------ + +To fully update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PUT`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PUT /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+==========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+----------------------------------------------------------+ +| id | url attribute | Product review id | ++---------------+----------------+----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++---------------+----------------+----------------------------------------------------------+ +| title | request | Product review title | ++---------------+----------------+----------------------------------------------------------+ +| comment | request | Product review comment | ++---------------+----------------+----------------------------------------------------------+ +| rating | request | Product review rating (1..5) | ++---------------+----------------+----------------------------------------------------------+ + +Example +^^^^^^^ + +To fully update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PUT \ + --data ' + { + "title": "A product review", + "rating": "4", + "comment": "This is a comment for review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +To partially update a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + PATCH /api/v1/products/{productCode}/reviews/{id} + ++------------------------------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++====================================+================+===========================================================+ +| Authorization | header | Token received during authentication | ++------------------------------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++------------------------------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be updated | ++------------------------------------+----------------+-----------------------------------------------------------+ +| title | request | Product review title | ++------------------------------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To partially update the product review with ``id = 1`` for the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Content-Type: application/json" \ + -X PATCH \ + --data ' + { + "title": "This is an another title for the review" + } + ' + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Deleting a Product Review +-------------------------- + +To delete a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}`` endpoint with the ``DELETE`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + DELETE /api/v1/products/{productCode}/reviews/{id} + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be deleted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To delete the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1 \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X DELETE + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Accept a Product Review +-------------------------- + +To accept a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/accept`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/accept + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be accepted | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To accept the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/accept \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + +Reject a Product Review +-------------------------- + +To reject a product review you will need to call the ``/api/v1/products/{productCode}/reviews/{id}/reject`` endpoint with the ``POST``, ``PUT`` or ``PATCH`` method. + +Definition +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/products/{productCode}/reviews/{id}/reject + ++---------------+----------------+-----------------------------------------------------------+ +| Parameter | Parameter type | Description | ++===============+================+===========================================================+ +| Authorization | header | Token received during authentication | ++---------------+----------------+-----------------------------------------------------------+ +| id | url attribute | Identifier of the product review | ++---------------+----------------+-----------------------------------------------------------+ +| productCode | url attribute | Code of product for which the reviews should be rejected | ++---------------+----------------+-----------------------------------------------------------+ + +Example +^^^^^^^ + +To reject the product review with ``id = 1`` from the product with ``code = MUG-TH`` use the below method. + +.. code-block:: bash + + $ curl http://demo.sylius.org/api/v1/products/MUG-TH/reviews/1/reject \ + -H "Authorization: Bearer SampleToken" \ + -H "Accept: application/json" \ + -X POST + +Exemplary Response +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + STATUS: 204 No Content + diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml index 723b525cfcb..05629014164 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/app/config.yml @@ -7,6 +7,7 @@ imports: - { resource: "@SyliusAdminApiBundle/Resources/config/grids/cart.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/payments.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product.yml" } + - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_review.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/product_variant.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/promotion.yml" } - { resource: "@SyliusAdminApiBundle/Resources/config/grids/shipments.yml" } diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml new file mode 100644 index 00000000000..ed74f66bc14 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/grids/product_review.yml @@ -0,0 +1,44 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski + +sylius_grid: + grids: + sylius_admin_api_product_review: + driver: + name: doctrine/orm + options: + class: "%sylius.model.product_review.class%" + repository: + method: createQueryBuilderByProductCode + arguments: ["%locale%", $productCode] + sorting: + date: desc + fields: + date: + type: datetime + label: sylius.ui.date + path: createdAt + sortable: createdAt + options: + format: d-m-Y H:i:s + title: + type: string + label: sylius.ui.title + sortable: ~ + rating: + type: string + label: sylius.ui.rating + sortable: ~ + status: + type: twig + label: sylius.ui.status + reviewSubject: + type: string + label: sylius.ui.product + author: + type: string + label: sylius.ui.customer + filters: + title: + type: string + label: sylius.ui.title diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml index 47b0cb85d8e..4fcf01b16f5 100644 --- a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/main.yml @@ -60,6 +60,10 @@ sylius_api_product_association_type: sylius_api_product_option: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_option.yml" +sylius_api_product_review: + resource: "@SyliusAdminApiBundle/Resources/config/routing/product_review.yml" + prefix: /products/{productCode} + sylius_api_product_taxon_position: resource: "@SyliusAdminApiBundle/Resources/config/routing/product_taxon_position.yml" diff --git a/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml new file mode 100644 index 00000000000..eab9184d239 --- /dev/null +++ b/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/product_review.yml @@ -0,0 +1,90 @@ +# This file is part of the Sylius package. +# (c) Paweł Jędrzejewski + +sylius_admin_api_product_review: + resource: | + path: 'reviews' + grid: sylius_admin_api_product_review + alias: sylius.product_review + section: admin_api + only: ['index'] + serialization_version: $version + vars: + route: + parameters: + productCode: $productCode + type: sylius.resource_api + +sylius_admin_api_product_review_create: + path: /reviews/ + methods: [POST] + defaults: + _controller: sylius.controller.product_review:createAction + _sylius: + serialization_groups: [Default, Detailed] + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + factory: + method: createForSubject + arguments: + - expr:notFoundOnNull(service('sylius.repository.product').findOneByCode($productCode)) + +sylius_admin_api_product_review_update: + path: /reviews/{id} + methods: [PUT, PATCH] + defaults: + _controller: sylius.controller.product_review:updateAction + _sylius: + serialization_version: $version + section: admin_api + form: Sylius\Bundle\CoreBundle\Form\Type\Product\ProductReviewType + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + +sylius_admin_api_product_review_delete: + path: /reviews/{id} + methods: [DELETE] + defaults: + _controller: sylius.controller.product_review:deleteAction + _sylius: + serialization_version: $version + section: admin_api + repository: + method: findOneByIdAndProductCode + arguments: [$id, $productCode] + csrf_protection: false + +sylius_admin_api_product_review_show: + path: /reviews/{code} + methods: [GET] + defaults: + _controller: sylius.controller.product_review:showAction + _sylius: + serialization_version: $version + section: admin_api + serialization_groups: [Default, Detailed] + repository: + method: findOneByIdAndProductCode + arguments: [$code, $productCode] + +sylius_admin_api_product_review_accept: + path: /reviews/{id}/accept + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: accept + +sylius_admin_api_product_review_reject: + path: /reviews/{id}/reject + methods: [POST, PUT, PATCH] + defaults: + _controller: sylius.controller.product_review:applyStateMachineTransitionAction + _sylius: + state_machine: + graph: sylius_product_review + transition: reject diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 3b27935b430..dd6a8333157 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -13,6 +13,7 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; +use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductReviewRepositoryInterface; @@ -60,4 +61,35 @@ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale ->getResult() ; } + + /** + * {@inheritdoc} + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder + { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->innerJoin('product.translations', 'translation') + ->andWhere('translation.locale = :locale') + ->andWhere('product.code = :productCode') + ->setParameter('locale', $locale) + ->setParameter('productCode', $productCode) + ; + } + + /** + * {@inheritdoc} + */ + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface + { + return $this->createQueryBuilder('o') + ->innerJoin('o.reviewSubject', 'product') + ->andWhere('product.code = :productCode') + ->andWhere('o.id = :id') + ->setParameter('productCode', $productCode) + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult() + ; + } } diff --git a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php index ee68e0c9512..8da1f32f05f 100644 --- a/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php +++ b/src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php @@ -13,6 +13,7 @@ namespace Sylius\Component\Core\Repository; +use Doctrine\ORM\QueryBuilder; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Review\Model\ReviewInterface; @@ -38,4 +39,20 @@ public function findLatestByProductId($productId, int $count): array; * @return array|ReviewInterface[] */ public function findAcceptedByProductSlugAndChannel(string $slug, string $locale, ChannelInterface $channel): array; + + /** + * @param string $locale + * @param string $productCode + * + * @return QueryBuilder + */ + public function createQueryBuilderByProductCode(string $locale, string $productCode): QueryBuilder; + + /** + * @param mixed $id + * @param string $productCode + * + * @return ReviewInterface|null + */ + public function findOneByIdAndProductCode($id, string $productCode): ?ReviewInterface; } diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php new file mode 100644 index 00000000000..0892a42c735 --- /dev/null +++ b/tests/Controller/ProductReviewApiTest.php @@ -0,0 +1,359 @@ + + */ +final class ProductReviewApiTest extends JsonApiTestCase +{ + /** + * @var array + */ + private static $authorizedHeaderWithContentType = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'CONTENT_TYPE' => 'application/json', + ]; + + /** + * @var array + */ + private static $authorizedHeaderWithAccept = [ + 'HTTP_Authorization' => 'Bearer SampleTokenNjZkNjY2MDEwMTAzMDkxMGE0OTlhYzU3NzYyMTE0ZGQ3ODcyMDAwM2EwMDZjNDI5NDlhMDdlMQ', + 'ACCEPT' => 'application/json', + ]; + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_list_when_access_is_denied() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product)); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'authentication/access_denied_response', Response::HTTP_UNAUTHORIZED); + } + + /** + * @test + */ + public function it_does_not_allow_to_show_product_review_when_it_does_not_exist() + { + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_showing_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/show_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_indexing_product_reviews() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/index_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_allows_create_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $data = +<<client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_response', Response::HTTP_CREATED); + } + + /** + * @test + */ + public function it_does_not_allow_to_create_product_review_without_required_fields() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('POST', $this->getReviewListUrl($product), [], [], static::$authorizedHeaderWithContentType, []); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/create_validation_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_does_not_allow_delete_product_review_if_it_does_not_exist() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('DELETE', $this->getReviewListUrl($product) . '0', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_delete_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('DELETE', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, []); + + $response = $this->client->getResponse(); + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + $this->client->request('GET', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'error/not_found_response', Response::HTTP_NOT_FOUND); + } + + /** + * @test + */ + public function it_allows_updating_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PUT', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_updating_partial_information_about_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + $this->loadFixturesFromFile('resources/locales.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $data = +<<client->request('PATCH', $this->getReviewUrl($product, $productReview), [], [], static::$authorizedHeaderWithContentType, $data); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_NO_CONTENT); + } + + /** + * @test + */ + public function it_allows_accept_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/accept_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_accept_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview3']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @test + */ + public function it_allows_reject_product_review() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview1']; + + $this->client->request('PATCH', $this->getReviewUrl($product, $productReview) . '/reject', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/reject_response', Response::HTTP_OK); + } + + /** + * @test + */ + public function it_does_not_allows_reject_product_review_if_it_has_not_new_status() + { + $this->loadFixturesFromFile('authentication/api_administrator.yml'); + $productReviewsData = $this->loadFixturesFromFile('resources/product_reviews.yml'); + + /** @var ProductInterface $product */ + $product = $productReviewsData['product1']; + + /** @var ReviewInterface $productReview */ + $productReview = $productReviewsData['productReview3']; + + $this->client->request('POST', $this->getReviewUrl($product, $productReview) . '/accept', [], [], static::$authorizedHeaderWithAccept); + $response = $this->client->getResponse(); + + $this->assertResponse($response, 'product_review/change_status_fail_response', Response::HTTP_BAD_REQUEST); + } + + /** + * @param ProductInterface $product + * + * @return string + */ + private function getReviewListUrl(ProductInterface $product): string + { + return sprintf('/api/v1/products/%s/reviews/', $product->getCode()); + } + + /** + * @param ProductInterface $product + * @param ReviewInterface $productReview + * + * @return string + */ + private function getReviewUrl(ProductInterface $product, ReviewInterface $productReview): string + { + return sprintf('%s%s', $this->getReviewListUrl($product), $productReview->getId()); + } +} diff --git a/tests/DataFixtures/ORM/resources/product_reviews.yml b/tests/DataFixtures/ORM/resources/product_reviews.yml new file mode 100644 index 00000000000..a70d6dd8204 --- /dev/null +++ b/tests/DataFixtures/ORM/resources/product_reviews.yml @@ -0,0 +1,44 @@ +Sylius\Component\Core\Model\Customer: + customer_example1: + firstName: Example + lastName: Queen + email: example.queen@example.com + emailCanonical: example.queen@example.com + customer_example2: + firstName: Example + lastName: King + email: example.king@example.com + emailCanonical: example.king@example.com + +Sylius\Component\Core\Model\Product: + product1: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_BEST + product2: + fallbackLocale: en_US + currentLocale: en + code: MUG_REVIEW_GOOD + +Sylius\Component\Core\Model\ProductReview: + productReview1: + title: mug_review_best + rating: 4 + comment: "mug_review_best_comment" + author: "@customer_example1" + status: "new" + reviewSubject: "@product1" + productReview2: + title: mug_review_good + rating: 3 + comment: "mug_review_good_comment" + author: "@customer_example1" + status: "new" + reviewSubject: "@product1" + productReview3: + title: mug_review_bad + rating: 1 + comment: "mug_review_bad_comment" + author: "@customer_example1" + status: "rejected" + reviewSubject: "@product1" diff --git a/tests/Responses/Expected/product_review/accept_response.json b/tests/Responses/Expected/product_review/accept_response.json new file mode 100644 index 00000000000..c3466545c45 --- /dev/null +++ b/tests/Responses/Expected/product_review/accept_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "accepted", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 4, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} diff --git a/tests/Responses/Expected/product_review/change_status_fail_response.json b/tests/Responses/Expected/product_review/change_status_fail_response.json new file mode 100644 index 00000000000..bc4332eb237 --- /dev/null +++ b/tests/Responses/Expected/product_review/change_status_fail_response.json @@ -0,0 +1,4 @@ +{ + "code":400, + "message":"" +} diff --git a/tests/Responses/Expected/product_review/create_response.json b/tests/Responses/Expected/product_review/create_response.json new file mode 100644 index 00000000000..80421db0cd4 --- /dev/null +++ b/tests/Responses/Expected/product_review/create_response.json @@ -0,0 +1,112 @@ +{ + "id": @integer@, + "title": "J_REVIEW", + "rating": 3, + "comment": "J_REVIEW_COMMENT", + "author": { + "id": @integer@, + "email": "j@example.com", + "emailCanonical": "j@example.com", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": [ + { + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + } + ], + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} diff --git a/tests/Responses/Expected/product_review/create_validation_fail_response.json b/tests/Responses/Expected/product_review/create_validation_fail_response.json new file mode 100644 index 00000000000..d35dab9c82b --- /dev/null +++ b/tests/Responses/Expected/product_review/create_validation_fail_response.json @@ -0,0 +1,39 @@ +{ + "code": 400, + "message": "Validation Failed", + "errors": { + "children": { + "rating": { + "errors": [ + "You must check review rating." + ], + "children": [ + {}, + {}, + {}, + {}, + {} + ] + }, + "title": { + "errors": [ + "Review title should not be blank." + ] + }, + "comment": { + "errors": [ + "Review comment should not be blank." + ] + }, + "author": { + "children": { + "email": { + "errors": [ + "Please enter your email." + ] + } + } + } + } + } +} diff --git a/tests/Responses/Expected/product_review/index_response.json b/tests/Responses/Expected/product_review/index_response.json new file mode 100644 index 00000000000..afa682d4ea2 --- /dev/null +++ b/tests/Responses/Expected/product_review/index_response.json @@ -0,0 +1,20 @@ +{ + "page": 1, + "limit": 10, + "pages": 1, + "total": 0, + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "first": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + }, + "last": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/reviews\/?page=1&limit=10" + } + }, + "_embedded": { + "items": [] + } +} diff --git a/tests/Responses/Expected/product_review/reject_response.json b/tests/Responses/Expected/product_review/reject_response.json new file mode 100644 index 00000000000..41460f1bbbc --- /dev/null +++ b/tests/Responses/Expected/product_review/reject_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "/api/v1/products/MUG_REVIEW_BEST" + }, + "variants": { + "href": "/api/v1/products/MUG_REVIEW_BEST/variants/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} diff --git a/tests/Responses/Expected/product_review/show_response.json b/tests/Responses/Expected/product_review/show_response.json new file mode 100644 index 00000000000..21ebc78c211 --- /dev/null +++ b/tests/Responses/Expected/product_review/show_response.json @@ -0,0 +1,92 @@ +{ + "id": @integer@, + "title": "mug_review_best", + "rating": 4, + "comment": "mug_review_best_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "reviewSubject": { + "id": @integer@, + "code": "MUG_REVIEW_BEST", + "attributes": [], + "options": [], + "associations": [], + "translations": { + "en": { + "locale": "en" + } + }, + "productTaxons": [], + "channels": [], + "reviews": { + "1": { + "id": @integer@, + "title": "mug_review_good", + "rating": 3, + "comment": "mug_review_good_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "new", + "createdAt": @string@, + "updatedAt": @string@ + }, + "2": { + "id": @integer@, + "title": "mug_review_bad", + "rating": 1, + "comment": "mug_review_bad_comment", + "author": { + "id": @integer@, + "email": "example.queen@example.com", + "emailCanonical": "example.queen@example.com", + "firstName": "Example", + "lastName": "Queen", + "gender": "u", + "_links": { + "self": { + "href": @string@ + } + } + }, + "status": "rejected", + "createdAt": @string@, + "updatedAt": @string@ + } + }, + "averageRating": 0, + "images": [], + "_links": { + "self": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST" + }, + "variants": { + "href": "\/api\/v1\/products\/MUG_REVIEW_BEST\/variants\/" + } + } + }, + "createdAt": @string@, + "updatedAt": @string@ +} From e386f379625308db79082e0ddaa6f41f0a322be4 Mon Sep 17 00:00:00 2001 From: Stoica Paul Date: Thu, 4 Jan 2018 20:33:37 +0200 Subject: [PATCH 22/22] product reviews api test remove author, fix indentation --- tests/Controller/ProductReviewApiTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Controller/ProductReviewApiTest.php b/tests/Controller/ProductReviewApiTest.php index c062ac04040..7f807e4b4fd 100644 --- a/tests/Controller/ProductReviewApiTest.php +++ b/tests/Controller/ProductReviewApiTest.php @@ -18,9 +18,6 @@ use Sylius\Component\Review\Model\ReviewInterface; use Symfony\Component\HttpFoundation\Response; -/** - * @author Paul Stoica - */ final class ProductReviewApiTest extends JsonApiTestCase { @@ -122,7 +119,7 @@ public function it_allows_creating_product_review() $product = $productReviewsData['product1']; $data = - <<