Add InPost Pay integration to admin templates

- Created a new template for the cart rule form with custom label, switch, and choice widgets.
- Implemented the InPost Pay block in the order details template for displaying delivery method, APM, and VAT invoice request.
- Added legacy support for the order details template to maintain compatibility with older PrestaShop versions.
This commit is contained in:
2025-09-14 14:38:09 +02:00
parent d895f86a03
commit 4066f6fa31
1086 changed files with 76598 additions and 6 deletions

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace izi\prestashop;
use izi\prestashop\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use izi\prestashop\DependencyInjection\Compiler\ProvideServiceLocatorFactoriesPass;
use izi\prestashop\DependencyInjection\Compiler\TaggedIteratorsCollectorPass;
use izi\prestashop\DependencyInjection\Dumper\PhpDumper;
use PrestaShopBundle\PrestaShopBundle;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollectionBuilder;
/**
* @internal
*/
final class AdminKernel extends Kernel
{
use MicroKernelTrait;
private $logDir;
private $cacheDir;
private $secret;
private $psVersion;
private $prestaShopBundle;
public function __construct(KernelInterface $kernel, string $psVersion)
{
$this->rootDir = $kernel->getRootDir();
$this->logDir = $kernel->getLogDir() . '/inpost/izi';
$this->cacheDir = $kernel->getCacheDir() . '/inpost/izi';
$this->secret = $kernel->getContainer()->getParameter('kernel.secret');
$this->psVersion = $psVersion;
$this->name = 'inpostizi';
parent::__construct($kernel->getEnvironment(), $kernel->isDebug());
}
public function registerBundles(): iterable
{
yield new FrameworkBundle();
yield new TwigBundle();
yield new SecurityBundle();
if (class_exists(SensioFrameworkExtraBundle::class)) {
yield new SensioFrameworkExtraBundle();
}
}
/**
* @override for the purpose of locating Twig templates included by PS using legacy bundle path syntax.
*/
public function getBundle($name, $first = true)
{
if ('PrestaShopBundle' === $name) {
$bundle = $this->getPrestaShopBundle();
return $first ? $bundle : [$bundle];
}
return parent::getBundle(...func_get_args());
}
public function getLogDir(): string
{
return $this->logDir;
}
public function getCacheDir(): string
{
return $this->cacheDir;
}
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$configDir = $this->getConfigDir();
$routes->import(sprintf('%s/routes.yml', $configDir));
$routes->addRoute($this->getConfigIndexRoute(), 'admin_inpost_izi_config_general');
}
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
$configDir = $this->getConfigDir();
$loader->load(sprintf('%s/services.yml', $configDir));
$loader->load(__DIR__ . '/Resources/config/admin.yml');
$container->loadFromExtension('framework', [
'secret' => $this->secret,
]);
if (\Tools::version_compare($this->psVersion, '1.7.4', '>=')) {
$loader->load(sprintf('%s/services/sf34.yml', $configDir));
} else {
$loader->load(sprintf('%s/services/sf28.yml', $configDir));
$loader->load(__DIR__ . '/Resources/config/admin28.yml');
$container->addCompilerPass(new ProvideServiceLocatorFactoriesPass('inpost.izi.service_locator'));
AnalyzeServiceReferencesPass::decorateRemovingPasses($container, 'inpost.izi.service_locator');
$container->addCompilerPass(new TaggedIteratorsCollectorPass());
}
}
protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, $class, $baseClass): void
{
if (\Tools::version_compare($this->psVersion, '1.7.4', '>=')) {
parent::dumpContainer(...func_get_args());
return;
}
$dumper = new PhpDumper($container);
$content = $dumper->dump([
'class' => $class,
'base_class' => $baseClass,
'file' => $cache->getPath(),
'debug' => $this->debug,
]);
$cache->write($content, $container->getResources());
}
private function getPrestaShopBundle(): BundleInterface
{
return $this->prestaShopBundle ?? ($this->prestaShopBundle = new PrestaShopBundle());
}
private function getConfigIndexRoute(): Route
{
return (new Route('/'))
->setMethods(['GET', 'POST'])
->setDefault('_controller', 'izi\prestashop\Controller\Admin\ConfigurationController:generalConfig');
}
private function getConfigDir(): string
{
return __DIR__ . '/../config';
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics;
class BasketAnalytics implements BasketAnalyticsInterface
{
/**
* @var int
*/
private $cartId;
/**
* @var string|null
*/
private $gclid;
/**
* @var string|null
*/
private $fbclid;
/**
* @var string|null
*/
private $client_id;
public function __construct(int $cartId, ?string $gclid, ?string $fbclid, ?string $client_id)
{
$this->cartId = $cartId;
$this->gclid = $gclid;
$this->fbclid = $fbclid;
$this->client_id = $client_id;
}
public function getCartId(): int
{
return $this->cartId;
}
public function getGclid(): ?string
{
return $this->gclid;
}
public function getFbclid(): ?string
{
return $this->fbclid;
}
public function getClientId(): ?string
{
return $this->client_id;
}
public function isEmpty(): bool
{
return null === $this->gclid && null === $this->fbclid && null === $this->client_id;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics;
interface BasketAnalyticsInterface
{
public function getGclid(): ?string;
public function getFbclid(): ?string;
public function getClientId(): ?string;
public function isEmpty(): bool;
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics;
class BasketAnalyticsParams implements BasketAnalyticsInterface
{
/**
* @var string|null
*/
private $gclid;
/**
* @var string|null
*/
private $fbclid;
/**
* @var string|null
*/
private $client_id;
public function __construct(?string $gclid, ?string $fbclid, ?string $client_id)
{
$this->gclid = $gclid;
$this->fbclid = $fbclid;
$this->client_id = $client_id;
}
public function getGclid(): ?string
{
return $this->gclid;
}
public function getFbclid(): ?string
{
return $this->fbclid;
}
public function getClientId(): ?string
{
return $this->client_id;
}
public function isEmpty(): bool
{
return null === $this->gclid && null === $this->fbclid && null === $this->client_id;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics;
use izi\prestashop\Database\Connection;
class BasketAnalyticsRepository implements BasketAnalyticsRepositoryInterface
{
public const TABLE_NAME = 'inpostizi_basket_analytics';
/**
* @var Connection
*/
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function add(BasketAnalytics $basketAnalytics): void
{
$this->connection->insert(self::TABLE_NAME, [
'cart_id' => $basketAnalytics->getCartId(),
'gclid' => $basketAnalytics->getGclid(),
'fbclid' => $basketAnalytics->getFbclid(),
'client_id' => $basketAnalytics->getClientId(),
]);
}
public function find(int $id): ?BasketAnalyticsInterface
{
$qb = $this->createQueryBuilder()->where('cart_id = ' . $id);
return $this->getOneOrNullResult($qb);
}
public function remove(int $id): void
{
$this->connection->delete(self::TABLE_NAME, [
'cart_id' => $id,
]);
}
public function save(BasketAnalytics $basketAnalytics): void
{
$exists = $this->find($basketAnalytics->getCartId());
if (null === $exists) {
$this->add($basketAnalytics);
return;
}
$this->connection->update(self::TABLE_NAME, [
'gclid' => $basketAnalytics->getGclid(),
'fbclid' => $basketAnalytics->getFbclid(),
'client_id' => $basketAnalytics->getClientId(),
], [
'cart_id' => $basketAnalytics->getCartId(),
]);
}
protected function createQueryBuilder(): \DbQuery
{
return (new \DbQuery())->from(self::TABLE_NAME);
}
protected function getOneOrNullResult(\DbQuery $qb): ?BasketAnalytics
{
if (false === $row = $this->connection->fetchAssociative((string) $qb)) {
return null;
}
return $this->hydrate($row);
}
protected function hydrate(array $row): BasketAnalytics
{
return new BasketAnalytics(
(int)$row['cart_id'],
$row['gclid'],
$row['fbclid'],
$row['client_id']
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics;
interface BasketAnalyticsRepositoryInterface
{
public function find(int $id): ?BasketAnalyticsInterface;
public function save(BasketAnalytics $basketAnalytics): void;
public function remove(int $id): void;
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Command;
use izi\prestashop\Analytics\BasketAnalyticsInterface;
/**
* @see UpdateCartAnalyticsHandler
*/
final class UpdateCartAnalyticsCommand
{
/**
* @var int
*/
private $cartId;
/**
* @var BasketAnalyticsInterface
*/
private $basketAnalytics;
public function __construct(int $cartId, BasketAnalyticsInterface $basketAnalytics)
{
$this->cartId = $cartId;
$this->basketAnalytics = $basketAnalytics;
}
public function getCartId(): int
{
return $this->cartId;
}
public function getBasketAnalytics(): BasketAnalyticsInterface
{
return $this->basketAnalytics;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use Symfony\Component\HttpFoundation\Request;
interface CookieEraserInterface
{
public function erase(Request $request): void;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use Symfony\Component\HttpFoundation\Request;
interface CookieExtractorInterface
{
public function extract(Request $request): ?string;
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use Symfony\Component\HttpFoundation\Request;
interface CookiePersisterInterface
{
public function persist(Request $request): void;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Executor;
use izi\prestashop\Analytics\Cookie\CookieEraserInterface;
use Symfony\Component\HttpFoundation\Request;
class CookieEraseExecutor implements CookieEraserInterface
{
/**
* @var iterable<CookieEraserInterface>
*/
private $erasers;
/**
* @param iterable<CookieEraserInterface> $erasers
*/
public function __construct(iterable $erasers)
{
$this->erasers = $erasers;
}
public function erase(Request $request): void
{
foreach ($this->erasers as $eraser) {
$eraser->erase($request);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Executor;
use izi\prestashop\Analytics\Cookie\CookiePersisterInterface;
use Symfony\Component\HttpFoundation\Request;
class CookiePersisterExecutor implements CookiePersisterInterface
{
/**
* @var iterable<CookiePersisterInterface>
*/
private $persisters;
/**
* @param iterable<CookiePersisterInterface> $persisters
*/
public function __construct(iterable $persisters)
{
$this->persisters = $persisters;
}
public function persist(Request $request): void
{
foreach ($this->persisters as $persister) {
$persister->persist($request);
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use izi\prestashop\Analytics\Cookie\Factory\CookieFactoryInterface;
use izi\prestashop\Analytics\Cookie\Repository\CookieRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
final class FacebookClickIdCookie implements CookieExtractorInterface, CookiePersisterInterface, CookieEraserInterface
{
private const COOKIE_NAME = 'izi_fbclid';
private const PARAMETER = 'fbclid';
private const COOKIE_EXPIRE_TIME = 3600;
/**
* @var CookieFactoryInterface
*/
private $cookieFactory;
/**
* @var CookieRepositoryInterface
*/
private $cookieRepository;
public function __construct(
CookieFactoryInterface $cookieFactory,
CookieRepositoryInterface $cookieRepository
) {
$this->cookieFactory = $cookieFactory;
$this->cookieRepository = $cookieRepository;
}
public function extract(Request $request): ?string
{
if ($request->cookies->has(self::COOKIE_NAME)) {
return $request->cookies->get(self::COOKIE_NAME);
}
return null;
}
public function persist(Request $request): void
{
$parameter = $request->query->get(self::PARAMETER);
if (null === $parameter) {
return;
}
$cookie = $this->cookieFactory->create(self::COOKIE_NAME, $parameter, time() + self::COOKIE_EXPIRE_TIME);
$this->cookieRepository->persist($cookie);
}
public function erase(Request $request): void
{
if ($request->cookies->has(self::COOKIE_NAME)) {
$cookie = $this->cookieFactory->create(self::COOKIE_NAME, '', -1);
unset($_COOKIE[self::COOKIE_NAME]);
$this->cookieRepository->persist($cookie);
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Factory;
use Symfony\Component\HttpFoundation\Cookie;
final class CookieFactory implements CookieFactoryInterface
{
public function create(
$name,
$value,
int $expire = 0,
string $path = '/',
string $domain = '',
bool $secure = false,
bool $httpOnly = true
): Cookie {
return new Cookie(
$name,
$value,
$expire,
$path,
$domain,
$secure,
$httpOnly
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Factory;
use Symfony\Component\HttpFoundation\Cookie;
interface CookieFactoryInterface
{
public function create(
$name,
$value,
int $expire = 0,
string $path = '/',
string $domain = '',
bool $secure = false,
bool $httpOnly = true
): Cookie;
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use izi\prestashop\Analytics\Cookie\Factory\CookieFactoryInterface;
use izi\prestashop\Analytics\Cookie\Repository\CookieRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
final class GoogleClickIdCookie implements CookieExtractorInterface, CookiePersisterInterface, CookieEraserInterface
{
private const COOKIE_NAME = 'izi_gclid';
private const PARAMETER = 'gclid';
private const COOKIE_EXPIRE_TIME = 3600;
/**
* @var CookieFactoryInterface
*/
private $cookieFactory;
/**
* @var CookieRepositoryInterface
*/
private $cookieRepository;
public function __construct(
CookieFactoryInterface $cookieFactory,
CookieRepositoryInterface $cookieRepository
) {
$this->cookieFactory = $cookieFactory;
$this->cookieRepository = $cookieRepository;
}
public function extract(Request $request): ?string
{
if ($request->cookies->has(self::COOKIE_NAME)) {
return $request->cookies->get(self::COOKIE_NAME);
}
return null;
}
public function persist(Request $request): void
{
$parameter = $request->query->get(self::PARAMETER);
if (null === $parameter) {
return;
}
$cookie = $this->cookieFactory->create(self::COOKIE_NAME, $parameter, time() + self::COOKIE_EXPIRE_TIME);
$this->cookieRepository->persist($cookie);
}
public function erase(Request $request): void
{
if ($request->cookies->has(self::COOKIE_NAME)) {
$cookie = $this->cookieFactory->create(self::COOKIE_NAME, '', -1);
unset($_COOKIE[self::COOKIE_NAME]);
$this->cookieRepository->persist($cookie);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie;
use Symfony\Component\HttpFoundation\Request;
final class GoogleClientIdCookie implements CookieExtractorInterface
{
private const COOKIE_NAME = '_ga';
public function extract(Request $request): ?string
{
if ($request->cookies->has(self::COOKIE_NAME)) {
$gaCookie = $request->cookies->get(self::COOKIE_NAME);
$parts = explode('.', $gaCookie);
if (count($parts) >= 3) {
return implode('.', array_slice($parts, 2));
}
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Repository;
use Symfony\Component\HttpFoundation\Cookie;
final class CookieRepository implements CookieRepositoryInterface
{
public function persist(Cookie $cookie): void
{
setcookie(
$cookie->getName(),
$cookie->getValue(),
$cookie->getExpiresTime(),
$cookie->getPath(),
$cookie->getDomain() ?? '',
$cookie->isSecure(),
$cookie->isHttpOnly()
);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Cookie\Repository;
use Symfony\Component\HttpFoundation\Cookie;
interface CookieRepositoryInterface
{
public function persist(Cookie $cookie): void;
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\EventListener;
use izi\prestashop\Analytics\Command\UpdateCartAnalyticsCommand;
use izi\prestashop\Analytics\Cookie\CookieEraserInterface;
use izi\prestashop\Analytics\Factory\BasketAnalyticsFactoryInterface;
use izi\prestashop\CommandBusInterface;
use izi\prestashop\Configuration\GeneralConfigurationInterface;
use izi\prestashop\Event\CartUpdatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
final class UpdateBasketAnalyticsListener implements EventSubscriberInterface
{
/**
* @var CommandBusInterface
*/
private $commandBus;
/**
* @var RequestStack
*/
private $requestStack;
/**
* @var BasketAnalyticsFactoryInterface
*/
private $basketAnalyticsFactory;
/**
* @var CookieEraserInterface
*/
private $cookieEraser;
/**
* @var GeneralConfigurationInterface
*/
private $generalConfiguration;
public function __construct(
CommandBusInterface $commandBus,
RequestStack $requestStack,
BasketAnalyticsFactoryInterface $basketAnalyticsFactory,
CookieEraserInterface $cookieEraser,
GeneralConfigurationInterface $generalConfiguration
) {
$this->commandBus = $commandBus;
$this->requestStack = $requestStack;
$this->basketAnalyticsFactory = $basketAnalyticsFactory;
$this->cookieEraser = $cookieEraser;
$this->generalConfiguration = $generalConfiguration;
}
public static function getSubscribedEvents(): array
{
return [
CartUpdatedEvent::class => 'onCartUpdated',
];
}
public function onCartUpdated(CartUpdatedEvent $event): void
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request || !$this->generalConfiguration->isSendAnalyticsData()) {
return;
}
$basketAnalytics = $this->basketAnalyticsFactory->createFromRequest($request);
if ($basketAnalytics->isEmpty()) {
return;
}
$this->commandBus->handle(new UpdateCartAnalyticsCommand((int) $event->getCart()->id, $basketAnalytics));
$this->cookieEraser->erase($request);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Factory;
use izi\prestashop\Analytics\BasketAnalyticsInterface;
use izi\prestashop\Analytics\BasketAnalyticsParams;
use izi\prestashop\Analytics\Cookie\CookieExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
final class BasketAnalyticsFactory implements BasketAnalyticsFactoryInterface
{
/**
* @var CookieExtractorInterface
*/
private $gclidExtractor;
/**
* @var CookieExtractorInterface
*/
private $fbclidExtractor;
/**
* @var CookieExtractorInterface
*/
private $clientIdExtractor;
public function __construct(
CookieExtractorInterface $gclidExtractor,
CookieExtractorInterface $fbclidExtractor,
CookieExtractorInterface $clientIdExtractor
) {
$this->gclidExtractor = $gclidExtractor;
$this->fbclidExtractor = $fbclidExtractor;
$this->clientIdExtractor = $clientIdExtractor;
}
public function createFromRequest(Request $request): BasketAnalyticsInterface
{
$gclid = $this->gclidExtractor->extract($request);
$fbclid = $this->fbclidExtractor->extract($request);
$clientId = $this->clientIdExtractor->extract($request);
return new BasketAnalyticsParams($gclid, $fbclid, $clientId);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Factory;
use izi\prestashop\Analytics\BasketAnalyticsInterface;
use Symfony\Component\HttpFoundation\Request;
interface BasketAnalyticsFactoryInterface
{
public function createFromRequest(Request $request): BasketAnalyticsInterface;
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Handler;
use izi\prestashop\Analytics\BasketAnalytics;
use izi\prestashop\Analytics\BasketAnalyticsRepositoryInterface;
use izi\prestashop\Analytics\Command\UpdateCartAnalyticsCommand;
final class UpdateCartAnalyticsHandler implements UpdateCartAnalyticsHandlerInterface
{
/**
* @var BasketAnalyticsRepositoryInterface
*/
private $repository;
public function __construct(BasketAnalyticsRepositoryInterface $repository)
{
$this->repository = $repository;
}
private function getNewFieldValue(?string $oldKey, ?string $newKey): ?string
{
if (null === $oldKey || $oldKey !== $newKey) {
return $newKey;
}
return $oldKey;
}
public function __invoke(UpdateCartAnalyticsCommand $command)
{
$currentBasketRepository = $this->repository->find($command->getCartId());
if (null === $currentBasketRepository) {
$basketAnalytics = new BasketAnalytics(
$command->getCartId(),
$command->getBasketAnalytics()->getGclid(),
$command->getBasketAnalytics()->getFbclid(),
$command->getBasketAnalytics()->getClientId()
);
$this->repository->save($basketAnalytics);
return;
}
$basketAnalytics = new BasketAnalytics(
$command->getCartId(),
$this->getNewFieldValue($currentBasketRepository->getGclid(), $command->getBasketAnalytics()->getGclid()),
$this->getNewFieldValue($currentBasketRepository->getFbclid(), $command->getBasketAnalytics()->getFbclid()),
$this->getNewFieldValue($currentBasketRepository->getClientId(), $command->getBasketAnalytics()->getClientId())
);
$this->repository->save($basketAnalytics);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Analytics\Handler;
use izi\prestashop\Analytics\Command\UpdateCartAnalyticsCommand;
interface UpdateCartAnalyticsHandlerInterface
{
public function __invoke(UpdateCartAnalyticsCommand $command);
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp;
use izi\prestashop\Http\Client\Factory\ClientFactoryInterface;
use izi\prestashop\Http\Client\Factory\GuzzleClientFactory;
use izi\prestashop\OAuth2\Authentication\ClientCredentialsInterface;
use izi\prestashop\OAuth2\Authentication\ClientSecretPost;
use izi\prestashop\OAuth2\AuthorizationProvider;
use izi\prestashop\OAuth2\AuthorizationProviderFactoryInterface;
use izi\prestashop\OAuth2\AuthorizationProviderInterface;
use izi\prestashop\OAuth2\AuthorizationServerClient;
use izi\prestashop\OAuth2\AuthorizationServerClientInterface;
use izi\prestashop\OAuth2\Grant\ClientCredentialsGrant;
use izi\prestashop\OAuth2\Token\AccessTokenRepositoryInterface;
use izi\prestashop\OAuth2\UriCollectionInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
final class AuthorizationProviderFactory implements AuthorizationProviderFactoryInterface
{
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var StreamFactoryInterface
*/
private $streamFactory;
/**
* @var ClientFactoryInterface
*/
private $clientFactory;
public function __construct(RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, ?ClientFactoryInterface $clientFactory = null)
{
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
$this->clientFactory = $clientFactory ?? new GuzzleClientFactory();
}
public function create(UriCollectionInterface $uriCollection, ClientCredentialsInterface $credentials, ?AccessTokenRepositoryInterface $tokenRepository = null): AuthorizationProviderInterface
{
$authSeverClient = $this->createAuthServerClient($uriCollection);
return new AuthorizationProvider(
$authSeverClient,
new ClientCredentialsGrant(),
$credentials,
$tokenRepository
);
}
private function createAuthServerClient(UriCollectionInterface $uriCollection): AuthorizationServerClientInterface
{
$httpClient = $this->clientFactory->create();
return new AuthorizationServerClient(
$httpClient,
$this->requestFactory,
$this->streamFactory,
$uriCollection,
new ClientSecretPost()
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket;
use izi\prestashop\BasketApp\Basket\Request\Basket;
use izi\prestashop\BasketApp\Basket\Response\BasketBindingKeyResponse;
use izi\prestashop\BasketApp\Basket\Response\BasketBindingResponse;
use izi\prestashop\BasketApp\Basket\Response\UpdateBasketResponse;
use izi\prestashop\BasketApp\Exception\BasketExpiredException;
use izi\prestashop\BasketApp\Exception\BasketNotBoundException;
use izi\prestashop\BasketApp\Exception\BasketNotFoundException;
interface BasketsApiClientInterface
{
/**
* @throws BasketNotFoundException
* @throws BasketExpiredException
* @throws BasketNotBoundException
*/
public function deleteBasketBinding(string $basketId, bool $orderCompleted = false): void;
/**
* @throws BasketExpiredException
*/
public function getBasketBinding(string $basketId, ?string $browserId = null): BasketBindingResponse;
public function initializeBasketBinding(string $basketId): BasketBindingKeyResponse;
/**
* @throws BasketNotFoundException
* @throws BasketExpiredException
*/
public function updateBasket(string $basketId, Basket $basket): UpdateBasketResponse;
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket\Request;
use izi\prestashop\Common\Basket\AvailablePromotion;
use izi\prestashop\Common\Basket\Consent;
use izi\prestashop\Common\Basket\DeliveryOption;
use izi\prestashop\Common\Basket\Product;
use izi\prestashop\Common\Basket\Summary;
use izi\prestashop\Common\PromoCode;
final class Basket implements \JsonSerializable
{
/**
* @var Summary
*/
private $summary;
/**
* @var DeliveryOption[]
*/
private $delivery;
/**
* @var PromoCode[]
*/
private $promo_codes;
/**
* @var Product[]
*/
private $products;
/**
* @var Product[]
*/
private $related_products;
/**
* @var Consent[]
*/
private $consents;
/**
* @var AvailablePromotion[]
*/
private $promotions_available;
/**
* @param DeliveryOption[] $delivery
* @param PromoCode[] $promo_codes
* @param Product[] $products
* @param Product[] $related_products
* @param Consent[] $consents
* @param AvailablePromotion[] $promotions_available
*/
public function __construct(Summary $summary, array $delivery, array $products, array $consents, array $promo_codes = [], array $related_products = [], array $promotions_available = [])
{
$this->summary = $summary;
$this->delivery = $delivery;
$this->promo_codes = $promo_codes;
$this->products = $products;
$this->related_products = $related_products;
$this->consents = $consents;
$this->promotions_available = $promotions_available;
}
public function getSummary(): Summary
{
return $this->summary;
}
/**
* @return DeliveryOption[]
*/
public function getDelivery(): array
{
return $this->delivery;
}
/**
* @return PromoCode[]
*/
public function getPromoCodes(): array
{
return $this->promo_codes;
}
/**
* @return Product[]
*/
public function getProducts(): array
{
return $this->products;
}
/**
* @return Product[]
*/
public function getRelatedProducts(): array
{
return $this->related_products;
}
/**
* @return Consent[]
*/
public function getConsents(): array
{
return $this->consents;
}
/**
* @return AvailablePromotion[]
*/
public function getPromotionsAvailable(): array
{
return $this->promotions_available;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket\Response;
final class BasketBindingKeyResponse implements \JsonSerializable
{
/**
* @var string
*/
private $basket_binding_api_key;
public function __construct(string $basket_binding_api_key)
{
$this->basket_binding_api_key = $basket_binding_api_key;
}
public function getBindingKey(): string
{
return $this->basket_binding_api_key;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket\Response;
final class BasketBindingResponse implements \JsonSerializable
{
/**
* @var bool
*/
private $basket_linked;
/**
* @var bool
*/
private $browser_trusted;
/**
* @var string|null
*/
private $inpost_basket_id;
/**
* @var ClientDetails|null
*/
private $client_details;
public function __construct(bool $basket_linked, bool $browser_trusted, ?string $inpost_basket_id = null, ?ClientDetails $client_details = null)
{
$this->basket_linked = $basket_linked;
$this->browser_trusted = $browser_trusted;
$this->inpost_basket_id = $inpost_basket_id;
$this->client_details = $client_details;
}
public function isBasketLinked(): bool
{
return $this->basket_linked;
}
public function isBrowserTrusted(): bool
{
return $this->browser_trusted;
}
public function getInPostBasketId(): ?string
{
return $this->inpost_basket_id;
}
public function getClientDetails(): ?ClientDetails
{
return $this->client_details;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket\Response;
use izi\prestashop\Common\PhoneNumber;
final class ClientDetails implements \JsonSerializable
{
/**
* @var PhoneNumber
*/
private $phone_number;
/**
* @var string
*/
private $masked_phone_number;
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $surname;
public function __construct(PhoneNumber $phone_number, string $masked_phone_number, string $name, string $surname)
{
$this->phone_number = $phone_number;
$this->masked_phone_number = $masked_phone_number;
$this->name = $name;
$this->surname = $surname;
}
public function getPhoneNumber(): PhoneNumber
{
return $this->phone_number;
}
public function getMaskedPhoneNumber(): string
{
return $this->masked_phone_number;
}
public function getName(): string
{
return $this->name;
}
public function getSurname(): string
{
return $this->surname;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Basket\Response;
final class UpdateBasketResponse implements \JsonSerializable
{
/**
* @var string
*/
private $inpost_basket_id;
public function __construct(string $inpost_basket_id)
{
$this->inpost_basket_id = $inpost_basket_id;
}
public function getInPostBasketId(): string
{
return $this->inpost_basket_id;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp;
use izi\prestashop\BasketApp\Basket\Request\Basket;
use izi\prestashop\BasketApp\Basket\Response\BasketBindingKeyResponse;
use izi\prestashop\BasketApp\Basket\Response\BasketBindingResponse;
use izi\prestashop\BasketApp\Basket\Response\UpdateBasketResponse;
use izi\prestashop\BasketApp\Exception\BasketAppException;
use izi\prestashop\BasketApp\Order\Request\OrderEvent;
use izi\prestashop\BasketApp\Payment\Response\AvailablePaymentOptions;
use izi\prestashop\BasketApp\Product\ProductsApiClientInterface;
use izi\prestashop\BasketApp\Product\Request\CreateProductsRequest;
use izi\prestashop\BasketApp\Product\Response\CreateProductsResponse;
use izi\prestashop\BasketApp\Product\Response\Product as ResponseProduct;
use izi\prestashop\BasketApp\Signature\Response\SigningKey;
use izi\prestashop\BasketApp\Signature\Response\SigningKeys;
use izi\prestashop\Common\Error\Error;
use izi\prestashop\Common\HotProduct\Product;
use izi\prestashop\Environment\ProductionEnvironment;
use izi\prestashop\Http\Exception\ClientException;
use izi\prestashop\Http\Exception\RedirectionException;
use izi\prestashop\Http\Exception\ServerException;
use izi\prestashop\Http\Util\UriResolver;
use izi\prestashop\Serializer\Normalizer\BasketAppPaginationPageDenormalizer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;
final class BasketAppClient implements BasketAppClientInterface, ProductsApiClientInterface
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var StreamFactoryInterface
*/
private $streamFactory;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var string
*/
private $baseUri;
public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, SerializerInterface $serializer, ?string $baseUri = null)
{
$this->client = $client;
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
$this->serializer = $serializer;
$this->baseUri = $baseUri ?? (new ProductionEnvironment())->getBasketAppApiUri();
}
public function updateBasket(string $basketId, Basket $basket): UpdateBasketResponse
{
$request = $this->createRequest('PUT', sprintf('/v2/izi/basket/%s', $basketId), $basket);
$response = $this->sendRequest($request);
return $this->deserialize($response, UpdateBasketResponse::class);
}
public function initializeBasketBinding(string $basketId): BasketBindingKeyResponse
{
$request = $this->createRequest('PUT', sprintf('/v2/izi/basket/%s/binding', $basketId));
$response = $this->sendRequest($request);
return $this->deserialize($response, BasketBindingKeyResponse::class);
}
public function deleteBasketBinding(string $basketId, bool $orderCompleted = false): void
{
$uri = sprintf('/v1/izi/basket/%s/binding', $basketId);
if ($orderCompleted) {
$uri .= '?' . self::buildQuery(['if_basket_realized' => (int) $orderCompleted]);
}
$request = $this->createRequest('DELETE', $uri);
$this->sendRequest($request, 204);
}
public function getBasketBinding(string $basketId, ?string $browserId = null): BasketBindingResponse
{
$uri = sprintf('/v1/izi/basket/%s/binding', $basketId);
if (null !== $browserId) {
$uri .= '?' . self::buildQuery(['browser_id' => $browserId]);
}
$request = $this->createRequest('GET', $uri);
$response = $this->sendRequest($request);
return $this->deserialize($response, BasketBindingResponse::class);
}
public function updateOrder(string $orderId, OrderEvent $event): void
{
$request = $this->createRequest('POST', sprintf('/v1/izi/order/%s/event', $orderId), $event);
// currently the API returns 201 on success instead of 200 given in the documentation
$this->sendRequest($request, 200, 201);
}
public function getSigningKey(string $version): SigningKey
{
$request = $this->createRequest('GET', sprintf('/v1/izi/signing-keys/public/%s', $version));
$response = $this->sendRequest($request);
return $this->deserialize($response, SigningKey::class);
}
public function getSigningKeys(): SigningKeys
{
$request = $this->createRequest('GET', '/v1/izi/signing-keys/public');
$response = $this->sendRequest($request);
return $this->deserialize($response, SigningKeys::class);
}
public function getAvailablePaymentOptions(): AvailablePaymentOptions
{
$request = $this->createRequest('GET', '/v1/izi/payment-methods');
$response = $this->sendRequest($request);
return $this->deserialize($response, AvailablePaymentOptions::class);
}
public function createProducts(CreateProductsRequest $products): CreateProductsResponse
{
$request = $this->createRequest('POST', '/v1/izi/products', $products);
$response = $this->sendRequest($request, 201);
return $this->deserialize($response, CreateProductsResponse::class);
}
/**
* @param string[] $productIds
*
* @return PaginationPage<ResponseProduct>
*/
public function getProductsPage(array $productIds = [], ?int $pageSize = null, ?int $pageIndex = null): PaginationPage
{
$params = [];
if (null !== $pageIndex) {
$params['page_index'] = $pageIndex;
}
if (null !== $pageSize) {
$params['page_size'] = $pageSize;
}
if ([] !== $productIds) {
$params['product_ids'] = implode(',', $productIds);
}
$uri = '/v1/izi/products';
if ([] !== $params) {
$uri .= '?' . self::buildQuery($params);
}
$request = $this->createRequest('GET', $uri);
$response = $this->sendRequest($request);
return $this->deserialize($response, PaginationPage::class, [
BasketAppPaginationPageDenormalizer::ITEM_TYPE_KEY => ResponseProduct::class,
]);
}
/**
* @param string[] $productIds
*
* @return \Generator<ResponseProduct>
*/
public function getProducts(array $productIds = [], ?int $pageSize = null): \Traversable
{
$pageIndex = 0;
do {
$page = $this->getProductsPage($productIds, $pageSize, $pageIndex++);
$pageSize = $page->getPageSize();
$totalCount = $page->getTotalCount();
foreach ($page as $product) {
yield $product;
}
} while ($totalCount > $pageSize * $pageIndex);
}
public function updateProduct(string $productId, Product $product): ResponseProduct
{
$request = $this->createRequest('PUT', sprintf('/v1/izi/product/%s', $productId), $product);
$response = $this->sendRequest($request);
return $this->deserialize($response, ResponseProduct::class);
}
public function deleteProduct(string $productId): void
{
$request = $this->createRequest('DELETE', sprintf('/v1/izi/product/%s', $productId));
$this->sendRequest($request, 204);
}
private function createRequest(string $method, string $uri, $payload = null): RequestInterface
{
$uri = UriResolver::resolve($uri, $this->baseUri);
$request = $this->requestFactory
->createRequest($method, $uri)
->withHeader('Accept', 'application/json');
if (null === $payload) {
return $request;
}
$payload = $this->serializer->serialize($payload, 'json', [
'datetime_format' => self::DATETIME_FORMAT,
'datetime_timezone' => self::DATETIME_ZONE,
]);
$body = $this->streamFactory->createStream($payload);
return $request
->withBody($body)
->withHeader('Content-Type', 'application/json');
}
private function sendRequest(RequestInterface $request, int $expectedStatusCode = 200, int ...$allowedStatusCodes): ResponseInterface
{
$response = $this->client->sendRequest($request);
$statusCode = $response->getStatusCode();
if (300 <= $statusCode) {
$this->handleUnsuccessfulResponse($request, $response);
}
if ($expectedStatusCode === $statusCode || in_array($statusCode, $allowedStatusCodes, true)) {
return $response;
}
throw new \UnexpectedValueException(sprintf('Unexpected server response code: %d', $statusCode));
}
/**
* @template T
*
* @param class-string<T> $class
*
* @return T
*/
private function deserialize(ResponseInterface $response, string $class, array $context = [])
{
return $this->serializer->deserialize((string) $response->getBody(), $class, 'json', $context);
}
private function handleUnsuccessfulResponse(RequestInterface $request, ResponseInterface $response): void
{
$statusCode = $response->getStatusCode();
try {
$error = $this->deserialize($response, Error::class);
throw BasketAppException::create($request, $error, $statusCode); // TODO? replace with exception factory service
} catch (ExceptionInterface $e) {
// ignore deserialization errors
}
if (500 <= $statusCode) {
throw new ServerException($request, $response);
}
if (400 <= $statusCode) {
throw new ClientException($request, $response);
}
throw new RedirectionException($request, $response);
}
private static function buildQuery(array $params): string
{
return http_build_query($params, '', '&', PHP_QUERY_RFC3986);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp;
use izi\prestashop\Configuration\ApiConfigurationInterface;
use izi\prestashop\Environment\AuthServerUriCollection;
use izi\prestashop\Http\Client\AuthorizingClient;
use izi\prestashop\Http\Client\Factory\ClientFactoryInterface;
use izi\prestashop\Http\Client\Factory\GuzzleClientFactory;
use izi\prestashop\OAuth2\AuthorizationProviderFactoryInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\Serializer\SerializerInterface;
final class BasketAppClientFactory
{
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var StreamFactoryInterface
*/
private $streamFactory;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var ClientFactoryInterface
*/
private $clientFactory;
/**
* @var AuthorizationProviderFactoryInterface
*/
private $authorizationProviderFactory;
public function __construct(RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, SerializerInterface $serializer, ?ClientFactoryInterface $clientFactory = null, ?AuthorizationProviderFactoryInterface $authorizationProviderFactory = null)
{
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
$this->serializer = $serializer;
$this->clientFactory = $clientFactory ?? new GuzzleClientFactory();
$this->authorizationProviderFactory = $authorizationProviderFactory ?? new AuthorizationProviderFactory($requestFactory, $streamFactory, $this->clientFactory);
}
public function create(ApiConfigurationInterface $configuration): BasketAppClient
{
$httpClient = $this->createHttpClient($configuration);
$baseUri = $configuration->getEnvironment()->getBasketAppApiUri();
return new BasketAppClient($httpClient, $this->requestFactory, $this->streamFactory, $this->serializer, $baseUri);
}
private function createHttpClient(ApiConfigurationInterface $configuration): ClientInterface
{
if (null === $credentials = $configuration->getClientCredentials()) {
throw new \RuntimeException('Client credentials are not available.');
}
$uriCollection = new AuthServerUriCollection($configuration->getEnvironment());
$authorizationProvider = $this->authorizationProviderFactory->create($uriCollection, $credentials);
$client = $this->clientFactory->create();
return new AuthorizingClient($client, $authorizationProvider);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp;
use izi\prestashop\BasketApp\Basket\BasketsApiClientInterface;
use izi\prestashop\BasketApp\Order\OrdersApiClientInterface;
use izi\prestashop\BasketApp\Payment\PaymentsApiClientInterface;
use izi\prestashop\BasketApp\Product\ProductsApiClientInterface;
use izi\prestashop\BasketApp\Signature\SigningKeysApiClientInterface;
/**
* @extends ProductsApiClientInterface
*/
interface BasketAppClientInterface extends BasketsApiClientInterface, OrdersApiClientInterface, SigningKeysApiClientInterface, PaymentsApiClientInterface
{
public const DATETIME_FORMAT = 'Y-m-d\TH:i:s.u\Z'; // format character "p" is not available before PHP 8.0
public const DATETIME_ZONE = 'UTC';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
class BadRequestException extends BasketAppException
{
public const ERROR_CODE = 'BAD_REQUEST';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class BasketAlreadyBoundException extends BasketAppException
{
public const ERROR_CODE = 'BASKET_IS_BINDED';
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
use izi\prestashop\BasketApp\Product\Exception as Product;
use izi\prestashop\Common\Error\Error;
use Psr\Http\Message\RequestInterface;
class BasketAppException extends \RuntimeException
{
/**
* @var array<string, class-string<self>> exception class names by error codes
*/
private const CLASS_MAP = [
BadRequestException::ERROR_CODE => BadRequestException::class,
MalformedRequestException::ERROR_CODE => MalformedRequestException::class,
UnauthorizedException::ERROR_CODE => UnauthorizedException::class,
ForbiddenException::ERROR_CODE => ForbiddenException::class,
ResourceNotFoundException::ERROR_CODE => ResourceNotFoundException::class,
BasketNotFoundException::ERROR_CODE => BasketNotFoundException::class,
PublicKeyNotFoundException::ERROR_CODE => PublicKeyNotFoundException::class,
OrderNotFoundException::ERROR_CODE => OrderNotFoundException::class,
MerchantDisabledException::ERROR_CODE => MerchantDisabledException::class,
BasketAlreadyBoundException::ERROR_CODE => BasketAlreadyBoundException::class,
PhoneBindingUnavailableException::ERROR_CODE => PhoneBindingUnavailableException::class,
BasketNotBoundException::ERROR_CODE => BasketNotBoundException::class,
BasketExpiredException::ERROR_CODE => BasketExpiredException::class,
CannotChangeOrderStatusException::ERROR_CODE => CannotChangeOrderStatusException::class,
InternalServerErrorException::ERROR_CODE => InternalServerErrorException::class,
Product\ProductNotFoundException::ERROR_CODE => Product\ProductNotFoundException::class,
Product\ProductExistsException::ERROR_CODE => Product\ProductExistsException::class,
Product\MaxProductLimitReachedException::ERROR_CODE => Product\MaxProductLimitReachedException::class,
];
/**
* @var RequestInterface
*/
private $request;
/**
* @var Error
*/
private $error;
public function __construct(RequestInterface $request, Error $error, int $statusCode)
{
$this->request = $request;
$this->error = $error;
parent::__construct($error->getMessage(), $statusCode);
}
public static function create(RequestInterface $request, Error $error, int $statusCode): self
{
$class = self::CLASS_MAP[$error->getCode()] ?? self::class;
return new $class($request, $error, $statusCode);
}
public function getRequest(): RequestInterface
{
return $this->request;
}
public function getError(): Error
{
return $this->error;
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class BasketExpiredException extends BasketAppException
{
public const ERROR_CODE = 'BASKET_EXPIRED';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class BasketNotBoundException extends BasketAppException
{
public const ERROR_CODE = 'BASKET_NOT_BOUND';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class BasketNotFoundException extends ResourceNotFoundException
{
public const ERROR_CODE = 'BASKET_NOT_FOUND';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class CannotChangeOrderStatusException extends BasketAppException
{
public const ERROR_CODE = 'STATUS_ORDER_ERROR';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class ForbiddenException extends BasketAppException
{
public const ERROR_CODE = 'FORBIDDEN';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class InternalServerErrorException extends BasketAppException
{
public const ERROR_CODE = 'INTERNAL_SERVER_ERROR';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class MalformedRequestException extends BadRequestException
{
public const ERROR_CODE = 'MALFORMED_REQUEST';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class MerchantDisabledException extends BasketAppException
{
public const ERROR_CODE = 'MERCHANT_DISABLE';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class OrderNotFoundException extends ResourceNotFoundException
{
public const ERROR_CODE = 'ORDER_NOT_FOUND';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class PhoneBindingUnavailableException extends BasketAppException
{
public const ERROR_CODE = 'PHONE_BINDING_METHOD_UNAVAILABLE';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class PublicKeyNotFoundException extends ResourceNotFoundException
{
public const ERROR_CODE = 'PUBLIC_KEY_NOT_FOUND';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
class ResourceNotFoundException extends BasketAppException
{
public const ERROR_CODE = 'NOT_FOUND';
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Exception;
final class UnauthorizedException extends BasketAppException
{
public const ERROR_CODE = 'UNAUTHORIZED';
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Order;
use izi\prestashop\BasketApp\Exception\CannotChangeOrderStatusException;
use izi\prestashop\BasketApp\Exception\OrderNotFoundException;
use izi\prestashop\BasketApp\Order\Request\OrderEvent;
interface OrdersApiClientInterface
{
/**
* @throws OrderNotFoundException
* @throws CannotChangeOrderStatusException
*/
public function updateOrder(string $orderId, OrderEvent $event): void;
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Order\Request;
use izi\prestashop\Common\Order\DeliveryAddress;
use izi\prestashop\Common\PhoneNumber;
final class Delivery implements \JsonSerializable
{
/**
* @var \DateTimeImmutable|null
*/
private $delivery_date;
/**
* @var string|null
*/
private $mail;
/**
* @var PhoneNumber|null
*/
private $phone_number;
/**
* @var string|null
*/
private $delivery_point;
/**
* @var DeliveryAddress|null
*/
private $delivery_address;
/**
* @var string|null
*/
private $courier_note;
public function __construct(?\DateTimeImmutable $delivery_date = null, ?string $mail = null, ?PhoneNumber $phone_number = null, ?string $delivery_point = null, ?DeliveryAddress $delivery_address = null, ?string $courier_note = null)
{
$this->delivery_date = $delivery_date;
$this->mail = $mail;
$this->phone_number = $phone_number;
$this->delivery_point = $delivery_point;
$this->delivery_address = $delivery_address;
$this->courier_note = $courier_note;
}
public function getDeliveryDate(): ?\DateTimeImmutable
{
return $this->delivery_date;
}
public function getEmail(): ?string
{
return $this->mail;
}
public function getPhoneNumber(): ?PhoneNumber
{
return $this->phone_number;
}
public function getPoint(): ?string
{
return $this->delivery_point;
}
public function getAddress(): ?DeliveryAddress
{
return $this->delivery_address;
}
public function getCourierNote(): ?string
{
return $this->courier_note;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Order\Request;
use izi\prestashop\Common\PhoneNumber;
final class OrderEvent implements \JsonSerializable
{
/**
* @var string
*/
private $event_id;
/**
* @var \DateTimeImmutable
*/
private $event_data_time;
/**
* @var PhoneNumber|null
*/
private $phone_number;
/**
* @var OrderEventData
*/
private $event_data;
public function __construct(string $event_id, \DateTimeImmutable $event_data_time, OrderEventData $event_data, ?PhoneNumber $phone_number = null)
{
$this->event_id = $event_id;
$this->event_data_time = $event_data_time;
$this->phone_number = $phone_number;
$this->event_data = $event_data;
}
public function getId(): string
{
return $this->event_id;
}
public function getDateTime(): \DateTimeImmutable
{
return $this->event_data_time;
}
public function getPhoneNumber(): ?PhoneNumber
{
return $this->phone_number;
}
public function getData(): OrderEventData
{
return $this->event_data;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Order\Request;
use izi\prestashop\Common\Order\MerchantOrderStatus;
final class OrderEventData implements \JsonSerializable
{
/**
* @var MerchantOrderStatus|null
*/
private $order_status;
/**
* @var string|null
*/
private $order_merchant_status_description;
/**
* @var string[]|null
*/
private $delivery_references_list;
/**
* @var Delivery|null
*/
private $delivery;
/**
* @param string[]|null $delivery_references_list
*/
public function __construct(?MerchantOrderStatus $order_status = null, ?string $order_merchant_status_description = null, ?array $delivery_references_list = null, ?Delivery $delivery = null)
{
$this->order_status = $order_status;
$this->order_merchant_status_description = $order_merchant_status_description;
$this->delivery_references_list = $delivery_references_list;
$this->delivery = $delivery;
}
public function getStatus(): ?MerchantOrderStatus
{
return $this->order_status;
}
public function getStatusDescription(): ?string
{
return $this->order_merchant_status_description;
}
/**
* @return string[]|null
*/
public function getDeliveryReferencesList(): ?array
{
return $this->delivery_references_list;
}
public function getDelivery(): ?Delivery
{
return $this->delivery;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp;
/**
* @template T
*
* @template-implements \IteratorAggregate<int, T>
*/
final class PaginationPage implements \IteratorAggregate, \JsonSerializable
{
/**
* @var T[]
*/
private $content;
/**
* @var int
*/
private $total_items;
/**
* @var int
*/
private $page_index;
/**
* @var int
*/
private $page_size;
/**
* @param T[] $content
*/
public function __construct(array $content, int $total_items, int $page_index, int $page_size)
{
$this->content = $content;
$this->total_items = $total_items;
$this->page_index = $page_index;
$this->page_size = $page_size;
}
/**
* @return T[]
*/
public function getItems(): array
{
return $this->content;
}
public function getTotalCount(): int
{
return $this->total_items;
}
public function getPageIndex(): int
{
return $this->page_index;
}
public function getPageSize(): int
{
return $this->page_size;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
/**
* @return \Traversable<int, T>
*/
public function getIterator(): \Traversable
{
return new \ArrayIterator($this->content);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Payment;
use izi\prestashop\BasketApp\Payment\Response\AvailablePaymentOptions;
interface PaymentsApiClientInterface
{
public function getAvailablePaymentOptions(): AvailablePaymentOptions;
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Payment\Response;
use izi\prestashop\Common\PaymentType;
/**
* @implements \IteratorAggregate<PaymentType>
*/
final class AvailablePaymentOptions implements \JsonSerializable, \IteratorAggregate
{
/**
* @var PaymentType[]
*/
private $payment_type;
/**
* @param PaymentType[] $payment_type
*/
public function __construct(array $payment_type)
{
$this->payment_type = $payment_type;
}
/**
* @return PaymentType[]
*/
public function getPaymentTypes(): array
{
return $this->payment_type;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
public function getIterator(): \Iterator
{
return new \ArrayIterator($this->payment_type);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Exception;
use izi\prestashop\BasketApp\Exception\BasketAppException;
final class MaxProductLimitReachedException extends BasketAppException
{
public const ERROR_CODE = 'MAX_LIMIT_PRODUCTS';
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Exception;
use izi\prestashop\BasketApp\Exception\BasketAppException;
final class ProductExistsException extends BasketAppException
{
public const ERROR_CODE = 'PRODUCT_EXISTS';
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Exception;
use izi\prestashop\BasketApp\Exception\BasketAppException;
final class ProductNotFoundException extends BasketAppException
{
public const ERROR_CODE = 'PRODUCT_NOT_FOUND';
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product;
use izi\prestashop\BasketApp\PaginationPage;
use izi\prestashop\BasketApp\Product\Exception\MaxProductLimitReachedException;
use izi\prestashop\BasketApp\Product\Exception\ProductExistsException;
use izi\prestashop\BasketApp\Product\Exception\ProductNotFoundException;
use izi\prestashop\BasketApp\Product\Request\CreateProductsRequest;
use izi\prestashop\BasketApp\Product\Response\CreateProductsResponse;
use izi\prestashop\BasketApp\Product\Response\Product as ResponseProduct;
use izi\prestashop\Common\HotProduct\Product;
interface ProductsApiClientInterface
{
/**
* Create products in InPost Pay.
*
* @throws ProductExistsException
* @throws MaxProductLimitReachedException
*/
public function createProducts(CreateProductsRequest $products): CreateProductsResponse;
/**
* Get paginated list of products from InPost Pay.
*
* @param string[] $productIds optional list of product IDs to filter by
*
* @return PaginationPage<ResponseProduct>
*/
public function getProductsPage(array $productIds = [], ?int $pageSize = null, ?int $pageIndex = null): PaginationPage;
/**
* Get InPost Pay products iterator.
*
* @param string[] $productIds optional list of product IDs to filter by
*
* @return \Traversable<ResponseProduct>
*/
public function getProducts(array $productIds = [], ?int $pageSize = null): \Traversable;
/**
* Update a product in InPost Pay.
*
* @throws ProductNotFoundException
*/
public function updateProduct(string $productId, Product $product): ResponseProduct;
/**
* Delete a product from InPost Pay.
*
* @throws ProductNotFoundException
*/
public function deleteProduct(string $productId): void;
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Request;
use izi\prestashop\Common\HotProduct\IdentifiableProduct;
final class CreateProductsRequest implements \JsonSerializable
{
/**
* @var IdentifiableProduct[]
*/
private $content;
/**
* @param IdentifiableProduct[] $content
*/
public function __construct(array $content)
{
$this->content = $content;
}
/**
* @return IdentifiableProduct[]
*/
public function getProducts(): array
{
return $this->content;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Response;
final class CreateProductsResponse implements \JsonSerializable
{
/**
* @var ProductId[]
*/
private $content;
/**
* @param ProductId[] $content
*/
public function __construct(array $content)
{
$this->content = $content;
}
/**
* @return ProductId[]
*/
public function getProductIds(): array
{
return $this->content;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Response;
use izi\prestashop\Common\Currency;
use izi\prestashop\Common\HotProduct\ProductAvailability;
use izi\prestashop\Common\HotProduct\Quantity;
use izi\prestashop\Common\Price;
use izi\prestashop\Common\Product\ProductAttribute;
use izi\prestashop\Common\Product\ProductImage;
final class Product implements \JsonSerializable
{
/**
* @var string
*/
private $product_id;
/**
* @var Status
*/
private $status;
/**
* @var string|null
*/
private $ean;
/**
* @var string|null
*/
private $qr_code;
/**
* @var string|null
*/
private $deep_link;
/**
* @var ProductAvailability|null
*/
private $product_availability;
/**
* @var string
*/
private $product_name;
/**
* @var string
*/
private $product_description;
/**
* @var string
*/
private $product_image;
/**
* @var ProductImage[]
*/
private $additional_product_images;
/**
* @var Price
*/
private $price;
/**
* @var Currency
*/
private $currency;
/**
* @var Quantity
*/
private $quantity;
/**
* @var ProductAttribute[]
*/
private $product_attributes;
/**
* @param ProductImage[] $additional_product_images
* @param ProductAttribute[] $product_attributes
*/
public function __construct(string $product_id, Status $status, string $product_name, string $product_description, string $product_image, Price $price, Currency $currency, Quantity $quantity, ?string $ean = null, ?string $qr_code = null, ?string $deep_link = null, ?ProductAvailability $product_availability = null, array $additional_product_images = [], array $product_attributes = [])
{
$this->product_id = $product_id;
$this->status = $status;
$this->product_name = $product_name;
$this->product_description = $product_description;
$this->product_image = $product_image;
$this->price = $price;
$this->currency = $currency;
$this->quantity = $quantity;
$this->ean = $ean;
$this->qr_code = $qr_code;
$this->deep_link = $deep_link;
$this->product_availability = $product_availability;
$this->additional_product_images = $additional_product_images;
$this->product_attributes = $product_attributes;
}
public function getId(): string
{
return $this->product_id;
}
public function getStatus(): Status
{
return $this->status;
}
public function getEan(): ?string
{
return $this->ean;
}
public function getQrCode(): ?string
{
return $this->qr_code;
}
public function getDeepLink(): ?string
{
return $this->deep_link;
}
public function getAvailability(): ?ProductAvailability
{
return $this->product_availability;
}
public function getName(): string
{
return $this->product_name;
}
public function getDescription(): string
{
return $this->product_description;
}
public function getImageUrl(): string
{
return $this->product_image;
}
/**
* @return ProductImage[]
*/
public function getAdditionalImages(): array
{
return $this->additional_product_images;
}
public function getPrice(): Price
{
return $this->price;
}
public function getCurrency(): Currency
{
return $this->currency;
}
public function getQuantity(): Quantity
{
return $this->quantity;
}
/**
* @return ProductAttribute[]
*/
public function getAttributes(): array
{
return $this->product_attributes;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Response;
final class ProductId implements \JsonSerializable
{
/**
* @var string
*/
private $product_id;
/**
* @var string|null
*/
private $qr_code;
/**
* @var string|null
*/
private $deep_link;
public function __construct(string $product_id, ?string $qr_code = null, ?string $deep_link = null)
{
$this->product_id = $product_id;
$this->qr_code = $qr_code;
$this->deep_link = $deep_link;
}
public function getId(): string
{
return $this->product_id;
}
public function getQrCode(): ?string
{
return $this->qr_code;
}
public function getDeepLink(): ?string
{
return $this->deep_link;
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Product\Response;
use izi\prestashop\Enum\StringEnum;
/**
* @method static self Active()
* @method static self Inactive()
*/
final class Status extends StringEnum
{
public const ACTIVE = 'ACTIVE';
public const INACTIVE = 'INACTIVE';
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Signature\Response;
use izi\prestashop\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
final class PublicKey implements \JsonSerializable, DenormalizableInterface
{
/**
* @var string
*/
private $public_key_base64;
/**
* @var string
*/
private $version;
/**
* @var string|null
*/
private $hash;
public function __construct(string $public_key_base64, string $version)
{
$this->public_key_base64 = $public_key_base64;
$this->version = $version;
}
public static function denormalize(array $data): self
{
if (!isset($data['public_key_base64'], $data['version'])) {
throw new UnexpectedValueException('Array data does not contain all of the required parameters.');
}
$key = new self($data['public_key_base64'], $data['version']);
if (isset($data['hash'])) {
$key->hash = $data['hash'];
}
return $key;
}
public function getBase64Encoded(): string
{
return $this->public_key_base64;
}
public function getVersion(): string
{
return $this->version;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
public function getHash(): string
{
return $this->hash ?? ($this->hash = hash('sha256', $this->public_key_base64));
}
public function getPemFormatted(): string
{
return "-----BEGIN PUBLIC KEY-----\n" . $this->public_key_base64 . "\n-----END PUBLIC KEY-----";
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Signature\Response;
final class SigningKey implements \JsonSerializable
{
/**
* @var string
*/
private $merchant_external_id;
/**
* @var PublicKey
*/
private $public_key;
public function __construct(string $merchant_external_id, PublicKey $public_key)
{
$this->merchant_external_id = $merchant_external_id;
$this->public_key = $public_key;
}
public function getMerchantId(): string
{
return $this->merchant_external_id;
}
public function getPublicKey(): PublicKey
{
return $this->public_key;
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Signature\Response;
/**
* @implements \IteratorAggregate<PublicKey>
*/
final class SigningKeys implements \IteratorAggregate, \JsonSerializable
{
/**
* @var string
*/
private $merchant_external_id;
/**
* @var PublicKey[]
*/
private $public_keys;
/**
* @param PublicKey[] $public_keys
*/
public function __construct(string $merchant_external_id, array $public_keys)
{
$this->merchant_external_id = $merchant_external_id;
$this->public_keys = $public_keys;
}
public function getMerchantId(): string
{
return $this->merchant_external_id;
}
/**
* @return PublicKey[]
*/
public function getPublicKeys(): array
{
return $this->public_keys;
}
public function getIterator(): \Iterator
{
return new \ArrayIterator($this->public_keys);
}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
public function getPublicKey(string $version): ?PublicKey
{
foreach ($this->public_keys as $publicKey) {
if ($version === $publicKey->getVersion()) {
return $publicKey;
}
}
return null;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\BasketApp\Signature;
use izi\prestashop\BasketApp\Exception\PublicKeyNotFoundException;
use izi\prestashop\BasketApp\Signature\Response\SigningKey;
use izi\prestashop\BasketApp\Signature\Response\SigningKeys;
interface SigningKeysApiClientInterface
{
/**
* @throws PublicKeyNotFoundException
*/
public function getSigningKey(string $version): SigningKey;
public function getSigningKeys(): SigningKeys;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\BasketApp\Basket\Request\Basket;
use izi\prestashop\Common\Basket\AvailablePromotion;
use izi\prestashop\Common\Basket\Consent;
use izi\prestashop\Common\Basket\DeliveryOption;
use izi\prestashop\Common\Basket\Product;
use izi\prestashop\Common\Basket\Summary;
use izi\prestashop\Common\PromoCode;
final class BasketAppRequestBuilder extends AbstractBasketBuilder implements BasketAppRequestBuilderInterface
{
public function build(): Basket
{
return parent::build();
}
/**
* @param DeliveryOption[] $delivery
* @param Product[] $products
* @param Consent[] $consents
* @param PromoCode[] $promoCodes
* @param Product[] $relatedProducts
* @param AvailablePromotion[] $availablePromotions
*/
protected function doBuild(Summary $summary, array $delivery, array $products, array $consents, array $promoCodes, array $relatedProducts, array $availablePromotions = []): Basket
{
return new Basket(
$summary,
$delivery,
$products,
$consents,
$promoCodes,
$relatedProducts,
$availablePromotions
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\BasketApp\Basket\Request\Basket;
/**
* @template-extends BasketBuilderInterface<Basket>
*/
interface BasketAppRequestBuilderInterface extends BasketBuilderInterface
{
public function build(): Basket;
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Builder\Basket\BasketAppRequestBuilderInterface as RequestBuilder;
use izi\prestashop\Builder\Basket\MerchantApiResponseBuilderInterface as ResponseBuilder;
use izi\prestashop\Configuration\ConsentsConfigurationInterface;
use izi\prestashop\Configuration\ProductConfigurationInterface;
use izi\prestashop\ContextManager;
use izi\prestashop\Entities\BasketInterface;
use izi\prestashop\Module\ModuleRepository;
use izi\prestashop\Product\Price\LowestPriceProviderFactory;
use izi\prestashop\Product\Price\LowestPriceProviderInterface;
use izi\prestashop\PromoCode\AvailablePromotionsProviderInterface;
use izi\prestashop\PromoCode\CartRulePromoCodeProvider;
use izi\prestashop\PromoCode\NullAvailablePromotionsProvider;
use izi\prestashop\PromoCode\PromoCodeProviderInterface;
use Psr\Clock\ClockInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class BasketBuilderFactory implements BasketBuilderFactoryInterface
{
/**
* @var ClockInterface
*/
private $clock;
/**
* @var ContextManager
*/
private $contextManager;
/**
* @var ConsentsConfigurationInterface
*/
private $consentsConfiguration;
/**
* @var ProductConfigurationInterface
*/
private $productConfiguration;
/**
* @var DeliveryFactory
*/
private $deliveryFactory;
/**
* @var ProductDeliveryFactory
*/
private $deliveryRelatedProductFactory;
/**
* @var LowestPriceProviderInterface
*/
private $lowestPriceProvider;
/**
* @var PromoCodeProviderInterface
*/
private $promoCodeProvider;
/**
* @var AvailablePromotionsProviderInterface
*/
private $availablePromotionsProvider;
/**
* @var ValidatorInterface|null
*/
private $validator;
public function __construct(
ClockInterface $clock,
ContextManager $contextManager,
ConsentsConfigurationInterface $consentsConfiguration,
ProductConfigurationInterface $productConfiguration,
DeliveryFactory $deliveryFactory,
ProductDeliveryFactory $deliveryRelatedProductFactory,
?LowestPriceProviderInterface $lowestPriceProvider = null,
?PromoCodeProviderInterface $promoCodeProvider = null,
?AvailablePromotionsProviderInterface $availablePromotionsProvider = null,
?ValidatorInterface $validator = null
) {
$this->clock = $clock;
$this->contextManager = $contextManager;
$this->consentsConfiguration = $consentsConfiguration;
$this->productConfiguration = $productConfiguration;
$this->deliveryFactory = $deliveryFactory;
$this->deliveryRelatedProductFactory = $deliveryRelatedProductFactory;
$this->lowestPriceProvider = $lowestPriceProvider ?? $this->createLowestPriceProvider();
$this->promoCodeProvider = $promoCodeProvider ?? CartRulePromoCodeProvider::create();
$this->availablePromotionsProvider = $availablePromotionsProvider ?? new NullAvailablePromotionsProvider();
$this->validator = $validator;
}
public function createRequestBuilder(BasketInterface $basket, ?int $shopId = null): RequestBuilder
{
$cart = $this->getCart($basket);
$builder = new BasketAppRequestBuilder(
$cart,
$this->contextManager,
$this->consentsConfiguration,
$this->productConfiguration,
$this->deliveryFactory,
$this->deliveryRelatedProductFactory,
null,
$this->lowestPriceProvider,
$this->promoCodeProvider,
$this->availablePromotionsProvider,
$this->validator
);
if (null !== $shopId) {
$builder->setShopId($shopId);
}
return $builder->setExpirationDate($this->getExpirationDate());
}
public function createResponseBuilder(BasketInterface $basket, ?int $shopId = null): ResponseBuilder
{
$cart = $this->getCart($basket);
$builder = new MerchantApiResponseBuilder(
$cart,
$this->contextManager,
$this->consentsConfiguration,
$this->productConfiguration,
$this->deliveryFactory,
$this->deliveryRelatedProductFactory,
null,
$this->lowestPriceProvider,
$this->promoCodeProvider,
$this->availablePromotionsProvider,
$this->validator
);
if (null !== $shopId) {
$builder->setShopId($shopId);
}
return $builder->setExpirationDate($this->getExpirationDate());
}
private function getCart(BasketInterface $basket): \Cart
{
$cart = $basket->getEntity();
if (!$cart instanceof \Cart) {
throw new \InvalidArgumentException(sprintf('Expected basket entity to be an instance of "%s", "%s" given.', \Cart::class, get_class($cart)));
}
return $cart;
}
// TODO configurable?
private function getExpirationDate(): \DateTimeImmutable
{
return $this->clock->now()->add(new \DateInterval('P2D'));
}
private function createLowestPriceProvider(): LowestPriceProviderInterface
{
$repository = new ModuleRepository();
$module = $repository->findByName('inpostizi');
$logger = $module ? $module->getLogger() : new NullLogger();
return (new LowestPriceProviderFactory($repository, $logger))->create();
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Builder\Basket\BasketAppRequestBuilderInterface as RequestBuilder;
use izi\prestashop\Builder\Basket\MerchantApiResponseBuilderInterface as ResponseBuilder;
use izi\prestashop\Entities\BasketInterface;
interface BasketBuilderFactoryInterface
{
public function createRequestBuilder(BasketInterface $basket): RequestBuilder;
public function createResponseBuilder(BasketInterface $basket): ResponseBuilder;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Common\Basket\Notice;
/**
* @template T of object
*/
interface BasketBuilderInterface
{
/**
* @return T
*/
public function build();
/**
* @return static
*/
public function setExpirationDate(?\DateTimeImmutable $expirationDate): self;
/**
* @return static
*/
public function setNotice(?Notice $notice): self;
/**
* @return static
*/
public function setAdditionalInformation(?string $info): self;
}

View File

@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Builder\PriceFactory;
use izi\prestashop\Common\Basket\DeliveryOption;
use izi\prestashop\Common\Delivery\DeliveryType;
use izi\prestashop\Common\Delivery\OptionalService;
use izi\prestashop\Common\Delivery\ServiceCode;
use izi\prestashop\Common\Price;
use izi\prestashop\Configuration\DTO\Shipping\ServiceOptions;
use izi\prestashop\Configuration\DTO\Shipping\ShippingOptions;
use izi\prestashop\Configuration\ShippingConfigurationInterface;
use izi\prestashop\ObjectModel\Repository\CarrierRepository;
use izi\prestashop\ObjectModel\Repository\ObjectRepositoryInterface;
use izi\prestashop\Shipping\DeliveryPriceCalculatorInterface;
use izi\prestashop\Translation\ServiceNameTranslator;
use Psr\Clock\ClockInterface;
class DeliveryFactory
{
/**
* @var ShippingConfigurationInterface
*/
private $configuration;
/**
* @var CarrierRepository
*/
private $carrierRepository;
/**
* @var ClockInterface
*/
private $clock;
/**
* @var ServiceNameTranslator
*/
private $serviceNameTranslator;
/**
* @var DeliveryPriceCalculatorInterface
*/
private $priceCalculator;
/**
* @param CarrierRepository $carrierRepository
*/
public function __construct(ShippingConfigurationInterface $configuration, ObjectRepositoryInterface $carrierRepository, ClockInterface $clock, ServiceNameTranslator $serviceNameTranslator, DeliveryPriceCalculatorInterface $priceCalculator)
{
$this->configuration = $configuration;
$this->carrierRepository = $carrierRepository;
$this->clock = $clock;
$this->serviceNameTranslator = $serviceNameTranslator;
$this->priceCalculator = $priceCalculator;
}
/**
* @return DeliveryOption[]
*/
public function getAvailableDeliveryOptions(\Cart $cart, ?int $idShop = null): array
{
$deliveryOptions = [];
$idShop = $idShop ?? (int) $cart->id_shop;
$deliveryDate = $this->getDeliveryDate();
$isFreeShipping = null;
foreach (DeliveryType::cases() as $deliveryType) {
$options = $this->configuration->getShippingOptions($deliveryType, (int) $idShop);
$referenceId = $options->getCarrierMapping()->getReferenceId();
if (null === $referenceId || null === $carrier = $this->getCarrier($cart, $referenceId)) {
continue;
}
if (!isset($isFreeShipping)) {
$isFreeShipping = $this->hasFreeShippingCartRule($cart);
}
$price = $isFreeShipping
? PriceFactory::create(0., 0.)
: $this->priceCalculator->getDeliveryPrice($cart, $carrier);
$deliveryOptions[] = new DeliveryOption(
$deliveryType,
$deliveryDate,
$price,
$this->getOptionalServices($deliveryType, $cart, $options, $carrier, $isFreeShipping),
$this->priceCalculator->getFreeDeliveryMinAmount($cart, $carrier)
);
}
return $deliveryOptions;
}
/**
* @return OptionalService[]
*/
private function getOptionalServices(DeliveryType $deliveryType, \Cart $cart, ShippingOptions $options, \Carrier $defaultCarrier, bool $isFreeShipping): array
{
$services = [];
foreach ($deliveryType->getAvailableServiceCodes() as $serviceCode) {
if (null === $carrierId = $options->getCarrierMapping($serviceCode)->getReferenceId()) {
continue;
}
$serviceOptions = $options->getServiceOptions($serviceCode);
if (null === $serviceOptions || !$this->checkServiceAvailability($serviceCode, $serviceOptions)) {
continue;
}
if ($carrierId === (int) $defaultCarrier->id_reference) {
$carrier = $defaultCarrier;
} elseif (null === $carrier = $this->getCarrier($cart, $carrierId)) {
continue;
}
$servicePrice = $this->getServicePrice($serviceOptions, $cart, $carrier, $defaultCarrier, $isFreeShipping);
if (0 > $servicePrice->getNet()) {
continue;
}
$services[] = new OptionalService(
$this->serviceNameTranslator->getName($serviceCode),
$serviceCode,
$servicePrice
);
}
return $services;
}
private function getServicePrice(ServiceOptions $options, \Cart $cart, \Carrier $carrier, \Carrier $defaultCarrier, bool $isFreeShipping): Price
{
if ($isFreeShipping) {
return PriceFactory::create(0., 0.);
}
$servicePrice = $this->priceCalculator->getAdditionalServicePrice($cart, $carrier, $options);
if ($carrier === $defaultCarrier) {
return $servicePrice;
}
$carrierPrice = $this->priceCalculator->getDeliveryPrice($cart, $carrier);
$defaultCarrierPrice = $this->priceCalculator->getDeliveryPrice($cart, $defaultCarrier);
return $servicePrice
->add($carrierPrice)
->sub($defaultCarrierPrice);
}
private function checkServiceAvailability(ServiceCode $serviceCode, ServiceOptions $options): bool
{
if (!$serviceCode->isAvailabilityTimeDependent()) {
return true;
}
$availability = $options->getAvailabilityRange();
return null === $availability
|| $availability->contains($this->clock->now());
}
private function isDeliveryOptionAvailable(\Cart $cart, int $carrierId): bool
{
$deliveryOptionList = $cart->getDeliveryOptionList();
$addressId = (int) $cart->id_address_delivery;
if (!isset($deliveryOptionList[$addressId])) {
return false;
}
foreach ($deliveryOptionList[$addressId] as $option) {
if (isset($option['carrier_list'][$carrierId]) && 1 === count($option['carrier_list'])) {
return true;
}
}
return false;
}
private function hasFreeShippingCartRule(\Cart $cart): bool
{
return [] !== $cart->getCartRules(\CartRule::FILTER_ACTION_SHIPPING, false);
}
private function getCarrier(\Cart $cart, int $referenceId): ?\Carrier
{
$carrier = $this->carrierRepository->findOneByReferenceId($referenceId);
if (null === $carrier || !$this->isDeliveryOptionAvailable($cart, (int) $carrier->id)) {
return null;
}
return $carrier;
}
// TODO make configurable?
private function getDeliveryDate(): \DateTimeImmutable
{
return $this->clock
->now()
->modify('+2 days')
->setTime(12, 0);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Common\Basket\AvailablePromotion;
use izi\prestashop\Common\Basket\Consent;
use izi\prestashop\Common\Basket\DeliveryOption;
use izi\prestashop\Common\Basket\Product;
use izi\prestashop\Common\Basket\Summary;
use izi\prestashop\Common\PromoCode;
use izi\prestashop\MerchantApi\Model\Basket\Response\Basket;
final class MerchantApiResponseBuilder extends AbstractBasketBuilder implements MerchantApiResponseBuilderInterface
{
public function build(): Basket
{
return parent::build();
}
/**
* @param DeliveryOption[] $delivery
* @param Product[] $products
* @param Consent[] $consents
* @param PromoCode[] $promoCodes
* @param Product[] $relatedProducts
* @param AvailablePromotion[] $availablePromotions
*/
protected function doBuild(Summary $summary, array $delivery, array $products, array $consents, array $promoCodes, array $relatedProducts, array $availablePromotions = []): Basket
{
return new Basket(
$summary,
$delivery,
$products,
$consents,
$promoCodes,
$relatedProducts,
$availablePromotions
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\MerchantApi\Model\Basket\Response\Basket;
/**
* @template-extends BasketBuilderInterface<Basket>
*/
interface MerchantApiResponseBuilderInterface extends BasketBuilderInterface
{
public function build(): Basket;
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Basket;
use izi\prestashop\Common\Basket\Quantity;
use izi\prestashop\Common\Basket\Summary;
use izi\prestashop\Common\Delivery\DeliveryType;
use izi\prestashop\Common\Dimensions;
use izi\prestashop\Common\Price;
use izi\prestashop\Common\Product\DeliveryProduct;
use izi\prestashop\Common\Product\DeliveryRelatedProducts;
use izi\prestashop\Common\Weight;
use izi\prestashop\Configuration\ShippingConfigurationInterface;
use izi\prestashop\ObjectModel\Repository\CarrierRepository;
use izi\prestashop\ObjectModel\Repository\ObjectRepositoryInterface;
use izi\prestashop\Shipping\CartTotal\CartTotalDeliveryStrategyInterface;
use izi\prestashop\Shipping\CartWeight\CartWeightDeliveryStrategyInterface;
use izi\prestashop\Shipping\ProductDimensions\ProductDimensionsDeliveryStrategyInterface;
use izi\prestashop\Shipping\ProductRestriction\ProductRestrictionDeliveryInterface;
class ProductDeliveryFactory
{
private $cartBaseWeight = [];
/**
* @var ShippingConfigurationInterface
*/
private $configuration;
/**
* @var ObjectRepositoryInterface<\Carrier>
*/
private $carrierRepository;
/**
* @var CartTotalDeliveryStrategyInterface
*/
private $cartTotalDeliveryStrategy;
/**
* @var CartWeightDeliveryStrategyInterface
*/
private $cartWeightDeliveryStrategy;
/**
* @var ProductDimensionsDeliveryStrategyInterface
*/
private $productDimensionsDeliveryStrategy;
/**
* @var ProductRestrictionDeliveryInterface
*/
private $productRestrictionDelivery;
/**
* @param CarrierRepository $carrierRepository
*/
public function __construct(
ShippingConfigurationInterface $configuration,
ObjectRepositoryInterface $carrierRepository,
CartTotalDeliveryStrategyInterface $cartTotalDeliveryStrategy,
CartWeightDeliveryStrategyInterface $cartWeightDeliveryStrategy,
ProductDimensionsDeliveryStrategyInterface $productDimensionsDeliveryStrategy,
ProductRestrictionDeliveryInterface $productRestrictionDelivery
) {
$this->configuration = $configuration;
$this->carrierRepository = $carrierRepository;
$this->cartTotalDeliveryStrategy = $cartTotalDeliveryStrategy;
$this->cartWeightDeliveryStrategy = $cartWeightDeliveryStrategy;
$this->productDimensionsDeliveryStrategy = $productDimensionsDeliveryStrategy;
$this->productRestrictionDelivery = $productRestrictionDelivery;
}
public function createForCartProduct(
DeliveryType $deliveryType,
\Cart $cart,
\Product $product,
Price $unitPrice,
float $weight,
Quantity $quantity,
?int $idShop = null
): DeliveryProduct {
$totalPrice = $unitPrice->multiply($quantity->getQuantity());
$totalWeight = (new Weight($weight))->multiply($quantity->getQuantity());
$isDeliveryOptionAvailable = $this->isDeliveryOptionAvailable($product, $deliveryType, $totalPrice, $totalWeight, $cart, $idShop);
return new DeliveryProduct($deliveryType, $isDeliveryOptionAvailable);
}
public function createForRelatedProduct(
DeliveryType $deliveryType,
Summary $summary,
\Cart $cart,
\Product $product,
Price $productPrice,
Quantity $quantity,
?float $freeDeliveryAmount = null,
?int $idShop = null
): DeliveryRelatedProducts {
$basketNewPrice = $this->getCartTotalWithNewProduct($productPrice, $quantity, $this->getCartBasePrice($summary));
$basketNewWeight = $this->getCartWeightWithNewProduct($product, $quantity, $cart);
$isDeliveryOptionAvailable = $this->isDeliveryOptionAvailable($product, $deliveryType, $basketNewPrice, $basketNewWeight, $cart, $idShop);
$isFreeDelivery = $isDeliveryOptionAvailable && $this->isFreeDelivery($freeDeliveryAmount, $basketNewPrice); // if delivery option is not available, free delivery is not possible
return new DeliveryRelatedProducts(
$deliveryType,
$isDeliveryOptionAvailable,
$isFreeDelivery
);
}
private function isDeliveryOptionAvailable(
\Product $product,
DeliveryType $deliveryType,
Price $basketNewPrice,
Weight $basketNewWeight,
\Cart $cart,
?int $idShop = null
): bool {
$idShop = $idShop ?? (int) $cart->id_shop;
$options = $this->configuration->getShippingOptions($deliveryType, (int) $idShop);
$referenceId = $options->getCarrierMapping()->getReferenceId();
if (null === $referenceId || null === $carrier = $this->carrierRepository->findOneByReferenceId($referenceId)) {
return false;
}
if (!$carrier->active) {
return false;
}
if (false === $this->productRestrictionDelivery->isShippingAvailableBasedOnProductCarrierRestriction($carrier, $product)) {
return false;
}
if (false === $this->productDimensionsDeliveryStrategy->isShippingAvailableBasedOnProductDimensions($carrier, new Dimensions((float) $product->width, (float) $product->height, (float) $product->depth))) {
return false;
}
if (false === $this->cartTotalDeliveryStrategy->isShippingAvailableBasedOnTotalPrice($carrier, $basketNewPrice)) {
return false;
}
if (false === $this->cartWeightDeliveryStrategy->isShippingAvailableBasedOnTotalWeight($carrier, $basketNewWeight)) {
return false;
}
// TODO: pass modified product list
return $this->checkModuleRestrictions($carrier, $cart);
}
private function isFreeDelivery(?float $freeDeliveryAmount, Price $basketNewPrice): bool
{
if (null === $freeDeliveryAmount) {
return false;
}
return $basketNewPrice->getGross() >= $freeDeliveryAmount;
}
private function getCartTotalWithNewProduct(Price $productPrice, Quantity $quantity, Price $basketBasePrice): Price
{
$productTotalPrice = $productPrice->multiply($quantity->getQuantity());
return $basketBasePrice->add($productTotalPrice);
}
private function getCartWeightWithNewProduct(\Product $product, Quantity $quantity, \Cart $cart): Weight
{
$productWeight = new Weight((float) $product->weight);
$defaultCombinationId = (int) \Product::getDefaultAttribute($product->id);
$cartWeight = $this->getCartBaseWeight($cart);
if (0 < $defaultCombinationId) {
$combination = new \Combination($defaultCombinationId);
$productWeight = $productWeight->add(new Weight((float) $combination->weight));
}
return $cartWeight->add($productWeight->multiply($quantity->getQuantity()));
}
private function getCartBasePrice(Summary $summary): Price
{
return $summary->getPromoPrice() ?? $summary->getBasePrice();
}
private function getCartBaseWeight(\Cart $cart): Weight
{
if (!isset($this->cartBaseWeight[$cart->id])) {
$this->cartBaseWeight[$cart->id] = new Weight((float) $cart->getTotalWeight());
}
return $this->cartBaseWeight[$cart->id];
}
/**
* Carrier modules can disable delivery options by returning false as the shipping cost.
*/
private function checkModuleRestrictions(\Carrier $carrier, \Cart $cart): bool
{
if (!$carrier->is_module || !$carrier->shipping_external || $carrier->is_free) {
return true;
}
$shippingCost = (\Closure::bind(function () use ($carrier) {
$products = $this->getProducts();
return $this->getPackageShippingCostFromModule($carrier, 10., $products);
}, $cart, \Cart::class))();
return false !== $shippingCost;
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Order;
use izi\prestashop\BasketApp\Order\Request\Delivery;
use izi\prestashop\BasketApp\Order\Request\OrderEvent;
use izi\prestashop\BasketApp\Order\Request\OrderEventData;
use izi\prestashop\Common\Order\MerchantOrderStatus;
use Psr\Clock\ClockInterface;
final class OrderEventBuilder implements OrderEventBuilderInterface
{
/**
* @var \Order
*/
private $order;
/**
* @var ClockInterface
*/
private $clock;
/**
* @var OrderStatusDescriptionProvider
*/
private $statusDescriptionProvider;
/**
* @var string|null
*/
private $eventId;
/**
* @var \DateTimeImmutable|null
*/
private $eventTime;
/**
* @var MerchantOrderStatus|null
*/
private $status;
/**
* @var string[]|null
*/
private $trackingNumbers;
/**
* @var Delivery|null
*/
private $delivery;
public function __construct(\Order $order, ClockInterface $clock, OrderStatusDescriptionProvider $orderStatusDescriptionProvider)
{
$this->order = $order;
$this->clock = $clock;
$this->statusDescriptionProvider = $orderStatusDescriptionProvider;
}
/**
* @return static
*/
public function setEventId(string $eventId): OrderEventBuilderInterface
{
$this->eventId = $eventId;
return $this;
}
/**
* @return static
*/
public function setEventTime(\DateTimeImmutable $time): OrderEventBuilderInterface
{
$this->eventTime = $time;
return $this;
}
/**
* @return static
*/
public function setOrderStatus(?MerchantOrderStatus $status): OrderEventBuilderInterface
{
$this->status = $status;
return $this;
}
/**
* @return static
*/
public function setTrackingNumbers(?array $numbers): OrderEventBuilderInterface
{
$this->trackingNumbers = $numbers;
return $this;
}
/**
* @return static
*/
public function setDeliveryData(?Delivery $delivery): OrderEventBuilderInterface
{
$this->delivery = $delivery;
return $this;
}
public function build(): OrderEvent
{
$eventData = $this->createEventData();
$eventTime = $this->getEventTime();
$eventId = $this->getEventId($eventTime);
return new OrderEvent($eventId, $eventTime, $eventData);
}
private function getEventId(\DateTimeImmutable $eventTime): string
{
return $this->eventId ?? (string) $eventTime->getTimestamp();
}
private function getEventTime(): \DateTimeImmutable
{
return $this->eventTime ?? $this->clock->now();
}
private function createEventData(): OrderEventData
{
$statusDescription = $this->statusDescriptionProvider->getStatus($this->order);
return new OrderEventData(
$this->status,
$statusDescription,
$this->trackingNumbers,
$this->delivery
);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Order;
use izi\prestashop\ObjectModel\Repository\ObjectRepositoryInterface;
use Psr\Clock\ClockInterface;
final class OrderEventBuilderFactory implements OrderEventBuilderFactoryInterface
{
/**
* @var ObjectRepositoryInterface<\Order>
*/
private $repository;
/**
* @var OrderStatusDescriptionProvider
*/
private $statusDescriptionProvider;
/**
* @var ClockInterface
*/
private $clock;
/**
* @param ObjectRepositoryInterface<\Order> $repository
*/
public function __construct(ObjectRepositoryInterface $repository, OrderStatusDescriptionProvider $statusDescriptionProvider, ClockInterface $clock)
{
$this->repository = $repository;
$this->statusDescriptionProvider = $statusDescriptionProvider;
$this->clock = $clock;
}
public function create(int $orderId): OrderEventBuilderInterface
{
if (null === $order = $this->repository->find($orderId)) {
throw new \DomainException(sprintf('Order "%s" does not exist.', $orderId));
}
return new OrderEventBuilder($order, $this->clock, $this->statusDescriptionProvider);
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Order;
interface OrderEventBuilderFactoryInterface
{
public function create(int $orderId): OrderEventBuilderInterface;
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Order;
use izi\prestashop\BasketApp\Order\Request\Delivery;
use izi\prestashop\BasketApp\Order\Request\OrderEvent;
use izi\prestashop\Common\Order\MerchantOrderStatus;
interface OrderEventBuilderInterface
{
public function build(): OrderEvent;
/**
* @return static
*/
public function setEventId(string $eventId): self;
/**
* @return static
*/
public function setEventTime(\DateTimeImmutable $time): self;
/**
* @return static
*/
public function setOrderStatus(?MerchantOrderStatus $status): self;
/**
* @param string[]|null $numbers
*
* @return static
*/
public function setTrackingNumbers(?array $numbers): self;
/**
* @param Delivery|null $delivery
*
* @return static
*/
public function setDeliveryData(?Delivery $delivery): self;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder\Order;
use izi\prestashop\Configuration\OrdersConfigurationInterface;
use izi\prestashop\ObjectModel\Repository\ObjectRepositoryInterface;
class OrderStatusDescriptionProvider
{
/**
* @var ObjectRepositoryInterface<\OrderState>
*/
private $repository;
/**
* @var OrdersConfigurationInterface
*/
private $configuration;
/**
* @param ObjectRepositoryInterface<\OrderState> $repository
*/
public function __construct(ObjectRepositoryInterface $repository, OrdersConfigurationInterface $configuration)
{
$this->repository = $repository;
$this->configuration = $configuration;
}
public function getStatus(\Order $order): string
{
$orderStateId = (int) $order->current_state;
$languageId = (int) $order->id_lang;
return $this->configuration->getStatusDescription($orderStateId, $languageId, (int) $order->id_shop)
?? $this->getOrderStateName($orderStateId, $languageId);
}
private function getOrderStateName(int $orderStateId, int $languageId): string
{
if (null === $orderState = $this->repository->find($orderStateId, $languageId)) {
throw new \RuntimeException(sprintf('Order state #%d does not exist.', $orderStateId));
}
return (string) $orderState->name;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Builder;
use izi\prestashop\Common\Price;
class PriceFactory
{
public static function create(float $net, float $gross): Price
{
$net = \Tools::ps_round($net, 2);
$gross = \Tools::ps_round($gross, 2);
$vat = \Tools::ps_round($gross - $net, 2);
return new Price($net, $gross, $vat);
}
}

View File

@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Cache;
use izi\prestashop\Cache\Exception\InvalidArgumentException;
use izi\prestashop\Cache\Exception\RuntimeException;
use izi\prestashop\Configuration\ConfigurationInterface;
use Psr\Clock\ClockInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*/
final class ConfigurationCache implements CacheInterface
{
private const CONFIG_KEY_PATTERN = 'INPOST_PAY_CACHE_%s';
/**
* @var ConfigurationInterface
*/
private $configuration;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var ClockInterface
*/
private $clock;
public function __construct(ConfigurationInterface $configuration, SerializerInterface $serializer, ClockInterface $clock)
{
$this->configuration = $configuration;
$this->serializer = $serializer;
$this->clock = $clock;
}
public static function getConfigKeyPrefix(): string
{
return sprintf(self::CONFIG_KEY_PATTERN, '');
}
/**
* @return mixed
*/
public function get($key, $default = null)
{
$configKey = $this->validateKey($key);
try {
$configValue = $this->configuration->get($configKey);
} catch (\Exception $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
try {
$item = $this->deserializeCacheItem($configValue);
} catch (ExceptionInterface $e) {
return $default;
}
if (null === $item) {
return $default;
}
if (null !== $item['expiry'] && $item['expiry'] < $this->clock->now()) {
return $default;
}
return $item['value'];
}
public function set($key, $value, $ttl = null): bool
{
$configKey = $this->validateKey($key);
try {
$item = $this->createCacheItem($value, $ttl);
} catch (ExceptionInterface $e) {
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
}
try {
$this->configuration->set($configKey, json_encode($item));
return true;
} catch (\Exception $e) {
return false;
}
}
public function delete($key): bool
{
$configKey = $this->validateKey($key);
try {
$this->configuration->remove($configKey);
return true;
} catch (\Exception $e) {
return false;
}
}
public function clear(): bool
{
try {
$this->configuration->removeMatching(sprintf(self::CONFIG_KEY_PATTERN, '*'));
return true;
} catch (\Exception $e) {
return false;
}
}
public function getMultiple($keys, $default = null): iterable
{
$this->validateKeys($keys);
$result = [];
foreach ($keys as $key) {
$value = $this->get($key, $default);
$result[$key] = $value;
}
return $result;
}
public function setMultiple($values, $ttl = null): bool
{
if (!is_iterable($values)) {
throw new InvalidArgumentException(sprintf('Cache values must be an array or a Traversable, "%s" given.', get_debug_type($values)));
}
$success = true;
foreach ($values as $key => $value) {
$success = $this->set($key, $value, $ttl) && $success;
}
return $success;
}
public function deleteMultiple($keys): bool
{
$this->validateKeys($keys);
$success = true;
foreach ($keys as $key) {
$success = $this->delete($key) && $success;
}
return $success;
}
public function has($key): bool
{
$configKey = $this->validateKey($key);
return $this->configuration->has($configKey);
}
private function validateKey($key): string
{
if (!is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be a string, "%s" given.', get_debug_type($key)));
}
if ('' === $key) {
throw new InvalidArgumentException('Cache key length must be greater than zero.');
}
$configKey = sprintf(self::CONFIG_KEY_PATTERN, $key);
if (!$this->configuration->isValidKey($configKey)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" is invalid.', $key));
}
return $configKey;
}
private function validateKeys($keys): void
{
if (!is_iterable($keys)) {
return;
}
throw new InvalidArgumentException(sprintf('Cache keys must be an array or a Traversable, "%s" given.', get_debug_type($keys)));
}
private function createCacheItem($value, $ttl): array
{
$expiry = $this->getExpiry($ttl);
if (is_object($value)) {
$class = get_class($value);
$value = $this->serializeValue($value);
} else {
$class = null;
}
return [
'value' => $value,
'class' => $class,
'expiry' => null !== $expiry ? $expiry->format('U.u') : null,
];
}
private function serializeValue($value): string
{
try {
return $this->serializer->serialize($value, 'json');
} catch (ExceptionInterface $e) {
throw new InvalidArgumentException('Unable to serialize cache value.', $e->getCode(), $e);
}
}
private function getExpiry($ttl): ?\DateTimeImmutable
{
if (null === $ttl) {
return null;
}
if (is_int($ttl)) {
$ttl = new \DateInterval(sprintf('PT%dS', $ttl));
} elseif (!$ttl instanceof \DateInterval) {
throw new InvalidArgumentException(sprintf('TTL must be an integer, a DateInterval or null, "%s" given.', get_debug_type($ttl)));
}
return $this->clock->now()->add($ttl);
}
private function deserializeCacheItem($value): ?array
{
if (null === $value) {
return null;
}
$item = json_decode((string) $value, true);
if (!is_array($item)) {
return null;
}
if (isset($item['class'])) {
$item['value'] = $this->serializer->deserialize($item['value'], $item['class'], 'json');
}
if (isset($item['expiry'])) {
$item['expiry'] = \DateTimeImmutable::createFromFormat('U.u', $item['expiry']);
}
return $item;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Cache\Exception;
use Psr\SimpleCache\InvalidArgumentException as InvalidArgumentExceptionInterface;
final class InvalidArgumentException extends \InvalidArgumentException implements InvalidArgumentExceptionInterface
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Cache\Exception;
use Psr\SimpleCache\CacheException;
final class RuntimeException extends \RuntimeException implements CacheException
{
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\CacheClearer;
use izi\prestashop\Repository\BasketSessionRepository;
final class BindingKeysCacheClearer implements CacheClearerInterface
{
/**
* @var BasketSessionRepository
*/
private $repository;
public function __construct(BasketSessionRepository $repository)
{
$this->repository = $repository;
}
public function clear(): void
{
$this->repository->resetBindingKeysCache();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\CacheClearer;
interface CacheClearerInterface
{
public function clear();
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\CacheClearer;
final class ChainCacheClearer implements CacheClearerInterface
{
/**
* @var iterable|CacheClearerInterface[]
*/
private $clearers;
/**
* @param iterable<CacheClearerInterface> $clearers
*/
public function __construct(iterable $clearers)
{
$this->clearers = $clearers;
}
public function clear(): void
{
foreach ($this->clearers as $clearer) {
$clearer->clear();
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\CacheClearer;
use Psr\SimpleCache\CacheInterface;
final class Psr16CacheClearer implements CacheClearerInterface
{
/**
* @var CacheInterface
*/
private $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
public function clear(): void
{
if ($this->cache->clear()) {
return;
}
throw new \RuntimeException('Failed to clear cache.');
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Cart\Exception;
final class ProductAlreadyInCartException extends \RuntimeException
{
/**
* @var array cart product
*/
private $product;
public function __construct(array $product, string $message = '', int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->product = $product;
}
public function getProduct(): array
{
return $this->product;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Cart\Util;
final class ProductHelper
{
public static function findProductInCart(\Cart $cart, int $productId, int $combinationId, int $customizationId = 0): ?array
{
foreach ($cart->getProducts() as $product) {
if (self::isSameProduct($product, $productId, $combinationId, $customizationId)) {
return $product;
}
}
return null;
}
public static function isInCart(\Cart $cart, int $productId, int $combinationId, int $customizationId = 0): bool
{
return null !== self::findProductInCart($cart, $productId, $combinationId, $customizationId);
}
public static function getCartQuantity(\Cart $cart, int $productId, int $combinationId, int $customizationId = 0): int
{
if (null === $product = self::findProductInCart($cart, $productId, $combinationId, $customizationId)) {
return 0;
}
return (int) $product['cart_quantity'];
}
private static function isSameProduct(array $cartProduct, int $productId, int $combinationId, int $customizationId): bool
{
return $productId === (int) $cartProduct['id_product']
&& $combinationId === (int) $cartProduct['id_product_attribute']
&& $customizationId === (int) $cartProduct['id_customization'];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Clock;
use Psr\Clock\ClockInterface;
final class SystemClock implements ClockInterface
{
private $timezone;
public function __construct(\DateTimeZone $timezone)
{
$this->timezone = $timezone;
}
public static function fromSystemTimezone(): self
{
return new self(new \DateTimeZone(date_default_timezone_get()));
}
public function now(): \DateTimeImmutable
{
return new \DateTimeImmutable('now', $this->timezone);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Command\Config;
use izi\prestashop\Handler\Config\CheckStatusHandler;
/**
* @see CheckStatusHandler
*/
final class CheckStatusCommand
{
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Command\Config;
use izi\prestashop\Handler\Config\DownloadModuleDataHandler;
/**
* @see DownloadModuleDataHandler
*/
final class DownloadModuleDataCommand
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Command\Config;
use izi\prestashop\Configuration\AdvancedConfigurationInterface;
use izi\prestashop\Handler\Config\UpdateAdvancedConfigurationHandler;
/**
* @see UpdateAdvancedConfigurationHandler
*/
final class UpdateAdvancedConfigurationCommand
{
/**
* @var AdvancedConfigurationInterface
*/
private $configuration;
public function __construct(AdvancedConfigurationInterface $configuration)
{
$this->configuration = $configuration;
}
public function getConfiguration(): AdvancedConfigurationInterface
{
return $this->configuration;
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace izi\prestashop\Command\Config;
use izi\prestashop\Handler\Config\UpdateCartRuleOptionsHandler;
use izi\prestashop\PromoCode\CartRuleOptions;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @see UpdateCartRuleOptionsHandler
*/
final class UpdateCartRuleOptionsCommand
{
/**
* @var int
*
* @Assert\GreaterThan(0)
*/
private $cartRuleId;
/**
* @var bool|null
*
* @Assert\NotNull()
*/
private $omnibus;
/**
* @var int|null
*
* @Assert\GreaterThan(0)
*/
private $promoDetailsPageId;
public function __construct(int $cartRuleId, ?bool $isOmnibus = false)
{
$this->cartRuleId = $cartRuleId;
$this->omnibus = $isOmnibus;
}
public static function for(CartRuleOptions $options): self
{
return (new self($options->getCartRuleId()))
->setOmnibus($options->isOmnibus())
->setPromoDetailsPageId($options->getPromoDetailsPageId());
}
public function getCartRuleId(): int
{
return $this->cartRuleId;
}
public function isOmnibus(): ?bool
{
return $this->omnibus;
}
public function setOmnibus(?bool $isOmnibus): self
{
$this->omnibus = $isOmnibus;
return $this;
}
public function getPromoDetailsPageId(): ?int
{
return $this->promoDetailsPageId;
}
public function setPromoDetailsPageId(?int $cmsId): self
{
$this->promoDetailsPageId = $cmsId;
return $this;
}
}

Some files were not shown because too many files have changed in this diff Show More