[ApiBundle] Implement Sales Statistics feature

This commit is contained in:
Rafikooo 2023-12-04 17:13:59 +01:00
parent a8b9c54e2c
commit 37ad43e0de
No known key found for this signature in database
GPG key ID: 4A26D8327BC2442B
12 changed files with 358 additions and 31 deletions

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Controller;
use Sylius\Bundle\ApiBundle\Query\Admin\GetSalesStatistics;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use Symfony\Component\Serializer\SerializerInterface;
use Webmozart\Assert\Assert;
final class GetSalesStatisticsAction
{
public function __construct(
private MessageBusInterface $queryBus,
private ChannelContextInterface $channelContext,
private SerializerInterface $serializer,
) {
}
public function __invoke(Request $request): Response
{
$channelCode = $request->query->get('channelCode') ?? $this->channelContext->getChannel()->getCode();
try {
$envelope = $this->queryBus->dispatch(
new GetSalesStatistics(channelCode: $channelCode),
);
} catch (HandlerFailedException $exception) {
return new JsonResponse(['error' => $exception->getPrevious()->getMessage()], Response::HTTP_BAD_REQUEST);
}
/** @var HandledStamp $handledStamp */
$handledStamp = $envelope->last(HandledStamp::class);
Assert::notNull($handledStamp);
return new JsonResponse($this->serializer->serialize($handledStamp->getResult(), 'json'), Response::HTTP_OK, [], true);
}
}

View file

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Query\Admin;
use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface;
use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface;
/** @experimental */
class GetSalesStatistics implements ChannelCodeAwareInterface, LocaleCodeAwareInterface
{
public ?\DateTimeInterface $startDate = null;
public ?\DateTimeInterface $endDate = null;
public function __construct(private ?string $localeCode = null, private ?string $channelCode = null)
{
}
public function getChannelCode(): ?string
{
return $this->channelCode;
}
public function setChannelCode(?string $channelCode): void
{
$this->channelCode = $channelCode;
}
public function getLocaleCode(): ?string
{
return $this->localeCode;
}
public function setLocaleCode(?string $localeCode): void
{
$this->localeCode = $localeCode;
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\QueryHandler\Admin;
use Sylius\Bundle\ApiBundle\Query\Admin\GetSalesStatistics;
use Sylius\Bundle\CoreBundle\Provider\SalesStatisticsProviderInterface;
use Sylius\Component\Channel\Repository\ChannelRepositoryInterface;
use Sylius\Component\Core\Dashboard\Interval;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\ValueObject\SalesStatistics;
/** @experimental */
final class GetSalesStatisticsHandler
{
/** @param ChannelRepositoryInterface<ChannelInterface> $channelRepository */
public function __construct(
private SalesStatisticsProviderInterface $salesStatisticsProvider,
private ChannelRepositoryInterface $channelRepository,
) {
}
public function __invoke(GetSalesStatistics $query): SalesStatistics
{
$startDate = new \DateTimeImmutable('first day of january this year');
$endDate = new \DateTimeImmutable('first day of january next year');
/** @var ChannelInterface|null $channel */
$channel = $this->channelRepository->findOneByCode($query->getChannelCode());
if ($channel === null) {
throw new \InvalidArgumentException(sprintf('Channel with code "%s" does not exist.', $query->getChannelCode()));
}
return $this->salesStatisticsProvider->provide(
$channel,
$startDate,
$endDate,
Interval::month(),
);
}
}

View file

@ -10,6 +10,12 @@ sylius_api_admin_authentication_token:
path: /admin/authentication-token
methods: ['POST']
sylius_api_admin_sales_statistics:
path: /admin/sales-statistics
methods: ['GET']
defaults:
_controller: Sylius\Bundle\ApiBundle\Controller\GetSalesStatisticsAction
sylius_api_shop_authentication_token:
path: /shop/authentication-token
methods: ['POST']

View file

@ -161,5 +161,11 @@
<service id="Sylius\Bundle\ApiBundle\Validator\ResourceInputDataPropertiesValidatorInterface" class="Sylius\Bundle\ApiBundle\Validator\ResourceApiInputDataPropertiesValidator">
<argument type="service" id="validator" />
</service>
<service id="Sylius\Bundle\ApiBundle\Controller\GetSalesStatisticsAction" public="true">
<argument type="service" id="sylius.query_bus" />
<argument type="service" id="sylius.context.channel" />
<argument type="service" id="serializer" />
</service>
</services>
</container>

View file

@ -16,7 +16,7 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
>
<services>
<defaults>
<defaults public="true">
<tag name="messenger.message_handler" bus="sylius.query_bus" />
</defaults>
@ -28,5 +28,11 @@
<service id="Sylius\Bundle\ApiBundle\QueryHandler\GetAddressLogEntryCollectionHandler">
<argument type="service" id="sylius.repository.address_log_entry" />
</service>
<service id="Sylius\Bundle\ApiBundle\QueryHandler\Admin\GetSalesStatisticsHandler">
<argument type="service" id="Sylius\Bundle\CoreBundle\Provider\SalesStatisticsProviderInterface" />
<argument type="service" id="sylius.repository.channel" />
<tag name="messenger.message_handler" bus="sylius.query_bus" />
</service>
</services>
</container>

View file

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Provider;
use Sylius\Component\Core\Dashboard\DashboardStatisticsProviderInterface;
use Sylius\Component\Core\Dashboard\Interval;
use Sylius\Component\Core\Dashboard\SalesDataProviderInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\ValueObject\SalesStatistics;
class SalesStatisticsProvider implements SalesStatisticsProviderInterface
{
public function __construct(
private DashboardStatisticsProviderInterface $statisticsProvider,
private SalesDataProviderInterface $salesDataProvider,
) {
}
public function provide(
ChannelInterface $channel,
\DateTimeInterface $startDate,
\DateTimeInterface $endDate,
Interval $interval,
): SalesStatistics {
$statistics = $this->statisticsProvider->getStatisticsForChannelInPeriod($channel, $startDate, $endDate);
$salesSummary = $this->salesDataProvider->getSalesSummary(
$channel,
$startDate,
$endDate,
$interval,
true,
);
return new SalesStatistics(
$salesSummary,
$interval,
$statistics->getNumberOfNewCustomers(),
$statistics->getNumberOfNewOrders(),
$statistics->getTotalSales(),
$statistics->getAverageOrderValue(),
);
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\CoreBundle\Provider;
use Sylius\Component\Core\Dashboard\Interval;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\ValueObject\SalesStatistics;
interface SalesStatisticsProviderInterface
{
public function provide(
ChannelInterface $channel,
\DateTimeInterface $startDate,
\DateTimeInterface $endDate,
Interval $interval,
): SalesStatistics;
}

View file

@ -35,5 +35,10 @@
<argument type="service" id="sylius.repository.locale" />
<argument type="string">%locale%</argument>
</service>
<service id="Sylius\Bundle\CoreBundle\Provider\SalesStatisticsProviderInterface" class="Sylius\Bundle\CoreBundle\Provider\SalesStatisticsProvider" >
<argument type="service" id="Sylius\Component\Core\Dashboard\DashboardStatisticsProviderInterface"/>
<argument type="service" id="Sylius\Component\Core\Dashboard\SalesDataProviderInterface"/>
</service>
</services>
</container>

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Core\ValueObject;
class SalesInPeriod
{
public function __construct(
private int $total,
private \DateTimeInterface $period,
) {
}
public function getPeriod(): \DateTimeInterface
{
return $this->period;
}
public function getTotal(): int
{
return $this->total;
}
}

View file

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Core\ValueObject;
use Sylius\Component\Core\Dashboard\Interval;
use Webmozart\Assert\Assert;
class SalesStatistics
{
/** @param SalesInPeriod[] $salesInPeriod */
public function __construct(
private array $salesInPeriod,
private Interval $intervalType,
private int $newCustomersCount,
private int $newOrdersCount,
private int $totalSales,
private int $averageOrderValue,
) {
Assert::allIsInstanceOf($salesInPeriod, SalesInPeriod::class);
}
/** @return SalesInPeriod[] */
public function getSalesInPeriod(): array
{
return $this->salesInPeriod;
}
public function getTotalSales(): int
{
return $this->totalSales;
}
public function getNewCustomersCount(): int
{
return $this->newCustomersCount;
}
public function getNewOrdersCount(): int
{
return $this->newOrdersCount;
}
public function getAverageOrderValue(): int
{
return $this->averageOrderValue;
}
public function getIntervalType(): string
{
return $this->intervalType->asString();
}
}

View file

@ -5,12 +5,6 @@
"alcohol/iso4217": {
"version": "4.0.0"
},
"amphp/amp": {
"version": "v2.5.0"
},
"amphp/byte-stream": {
"version": "v1.8.0"
},
"api-platform/core": {
"version": "2.5",
"recipe": {
@ -49,15 +43,9 @@
"coduo/php-to-string": {
"version": "3.0.0"
},
"composer/pcre": {
"version": "3.0.0"
},
"composer/semver": {
"version": "1.5.1"
},
"composer/xdebug-handler": {
"version": "1.4.3"
},
"dealerdirect/phpcodesniffer-composer-installer": {
"version": "v0.7.0"
},
@ -67,9 +55,6 @@
"dmore/chrome-mink-driver": {
"version": "2.7.0"
},
"dnoegel/php-xdg-base-dir": {
"version": "v0.1.1"
},
"doctrine/annotations": {
"version": "1.0",
"recipe": {
@ -161,12 +146,6 @@
"fakerphp/faker": {
"version": "v1.12.0"
},
"felixfbecker/advanced-json-rpc": {
"version": "v3.1.1"
},
"felixfbecker/language-server-protocol": {
"version": "v1.4.0"
},
"friends-of-behat/mink": {
"version": "v1.8.0"
},
@ -358,9 +337,6 @@
"config/packages/test/nelmio_alice.yaml"
]
},
"netresearch/jsonmapper": {
"version": "v2.1.0"
},
"nikic/php-parser": {
"version": "v4.6.0"
},
@ -466,9 +442,6 @@
"polishsymfonycommunity/symfony-mocker-container": {
"version": "v1.0.2"
},
"psalm/plugin-mockery": {
"version": "0.3.0"
},
"psr/cache": {
"version": "1.0.1"
},
@ -985,9 +958,6 @@
"twig/twig": {
"version": "v2.13.1"
},
"vimeo/psalm": {
"version": "3.11.6"
},
"webmozart/assert": {
"version": "1.9.1"
},