<?php
namespace Drupal\bb2b_baselinker;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Extension\InfoParserInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\RequeueException;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Theme\ThemeInitializationInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use GuzzleHttp\RequestOptions;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* BaseLinker Sync Service.
*/
class SyncService {
use StringTranslationTrait;
/**
* Get orders method name.
*
* @var string
*/
const GET_ORDERS_METHOD = 'getOrders';
/**
* Last order confirmed time key.
*
* @var string
*/
const LAST_ORDER_CONFIRMED_TIME = 'bb2b_baselinker.last_order_confirmed_time';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The config for athenapdf.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $athenapdfConfig;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The http client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The order queue.
*
* @var \Drupal\Core\Queue\QueueFactory
*/
protected $getOrderQueue;
/**
* The state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The extension path resolver.
*
* @var \Drupal\Core\Extension\ExtensionPathResolver
*/
protected $extensionPathResolver;
/**
* The info parser.
*
* @var \Drupal\Core\Extension\InfoParserInterface
*/
protected $infoParser;
/**
* The theme initialization.
*
* @var \Drupal\Core\Theme\ThemeInitializationInterface
*/
protected $themeInitialization;
/**
* The asset resolver.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected $assetResolver;
/**
* The CSS collection renderer.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $cssCollectionRenderer;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* File system.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The file URL generator.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* Constructs a new SyncService object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory_athenapdf
* The configuration factory for athenapdf api.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger.
* @param \GuzzleHttp\Client $http_client
* The http client.
* @param \Drupal\Core\Queue\QueueFactory $queue_factory
* The queue factory.
* @param \Drupal\Core\State\StateInterface $state
* The state.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Extension\ExtensionPathResolver $extension_path_resolver
* The extension path resolver.
* @param \Drupal\Core\Extension\InfoParserInterface $info_parser
* The info parser.
* @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
* The theme initialization.
* @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
* The asset resolver.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
* The CSS collection renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* File System.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
* The file URL generator.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
ConfigFactoryInterface $config_factory,
ConfigFactoryInterface $config_factory_athenapdf,
LoggerChannelFactoryInterface $logger_factory,
Client $http_client,
QueueFactory $queue_factory,
StateInterface $state,
Connection $database,
RendererInterface $renderer,
ThemeHandlerInterface $theme_handler,
ExtensionPathResolver $extension_path_resolver,
InfoParserInterface $info_parser,
ThemeInitializationInterface $theme_initialization,
AssetResolverInterface $asset_resolver,
AssetCollectionRendererInterface $css_collection_renderer,
RequestStack $request_stack,
FileSystemInterface $fileSystem,
FileUrlGeneratorInterface $file_url_generator
) {
$this->entityTypeManager = $entity_type_manager;
$this->config = $config_factory->get('bb2b_baselinker.settings');
$this->athenapdfConfig = $config_factory_athenapdf->get('athenapdf_api.settings');
$this->logger = $logger_factory->get('bb2b_baselinker');
$this->httpClient = $http_client;
$this->getOrderQueue = $queue_factory->get('get_orders_from_baselinker');
$this->addProductQueue = $queue_factory->get('bb2b_baselinker_addproduct');
$this->state = $state;
$this->database = $database;
$this->renderer = $renderer;
$this->themeHandler = $theme_handler;
$this->extensionPathResolver = $extension_path_resolver;
$this->infoParser = $info_parser;
$this->themeInitialization = $theme_initialization;
$this->assetResolver = $asset_resolver;
$this->cssCollectionRenderer = $css_collection_renderer;
$this->request = $request_stack->getCurrentRequest();
$this->fileSystem = $fileSystem;
$this->fileUrlGenerator = $file_url_generator;
}
/**
* Get orders from BaseLinker API.
*
* A maximum of 100 orders are returned at a time.
*/
public function getOrders() {
// The buyer's data are deleted after 30 days of shipment.
$date_confirmed_from = $this->state
->get(self::LAST_ORDER_CONFIRMED_TIME, strtotime('-30 days'));
$parameters = [
'date_confirmed_from' => $date_confirmed_from,
];
$items = $this->getItems(self::GET_ORDERS_METHOD, $parameters);
$api_orders = $items['orders'] ?? [];
if ($api_orders && $items['status'] === 'SUCCESS') {
foreach ($api_orders as $order) {
if ($order['delivery_fullname'] !== '-') {
$query = $this->database->select('queue', 'q');
$query->condition('name', 'get_orders_from_baselinker');
$query->condition('data', serialize($order));
$query->fields('q', ['item_id']);
// Skip same queue items.
if (empty($query->execute()->fetchAll())) {
$this->getOrderQueue->createItem($order);
}
}
}
}
}
/**
* Sync products to BaseLinker API.
*/
public function addProduct() {
// BaseLinker API credentials.
$data = [];
$data['token'] = $this->config->get('token');
$data['connected_domain'] = $this->config->get('connected_domain');
// Get products that we want to integrate in BaseLinker.
$products = $this->entityTypeManager
->getStorage('commerce_product')
->loadMultiple();
// Send request to BaseLinker.
foreach ($products as $product) {
$data['product'] = $product;
$params = [
'storage_id' => '4721',
'product_id' => $data['product']->id(),
];
try {
$response = $this->httpClient->request('post', $data['connected_domain'], [
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'X-BLToken' => $data['token'],
],
'form_params' => [
'method' => 'addProduct',
'parameters' => $params,
]
]);
}
catch (RequeueException $e) {
$this->logger->debug('BaseLinker request failed with the message: @message', [
'@message' => $e->getMessage(),
]);
}
$response = json_decode($response->getStatusCode());
}
}
/**
* Request to the connected domain.
*
* @param string $method
* Which method is used for request.
* @param array $parameters
* Order parameters for request.
*
* @return array
* Return items.
*/
public function getItems(string $method, array $parameters = []): array {
$token = $this->config->get('token');
$connected_domain = $this->config->get('connected_domain');
$base_parameters = [];
if ($method === self::GET_ORDERS_METHOD) {
$base_parameters = [
'get_unconfirmed_orders' => FALSE,
];
}
$parameters = array_merge($base_parameters, $parameters);
try {
$response = $this->httpClient->request('post', $connected_domain, [
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'X-BLToken' => $token,
],
'form_params' => [
'method' => $method,
'parameters' => json_encode($parameters),
],
'verify' => FALSE,
'http_errors' => FALSE,
]);
}
catch (RequestException $e) {
$this->logger->debug('BaseLinker request failed with the message: @message', [
'@message' => $e->getMessage(),
]);
}
$this->logger->debug('BaseLinker API response: <pre>@body</pre>', [
'@body' => $response ? $response->getBody()->getContents() : '',
]);
$items = json_decode($response->getBody(), TRUE);
return $items ?: [];
}
/**
* Generate PDF file.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* Order Interface.
* @param bool $is_html
* Html.
* @param bool $return_with_file
* Return with file.
*
* @return string|\Symfony\Component\HttpFoundation\Response|null
* Retrun response.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generatePdf(OrderInterface $order, bool $is_html = FALSE, bool $return_with_file = FALSE) {
$renderer = $this->renderer;
$theme = $this->themeHandler->getDefault();
$theme_path = $this->extensionPathResolver->getPath('theme', $theme);
$theme_info = $this->infoParser->parse("$theme_path/$theme.info.yml");
$pdf_title = $this->t('Baselinker');
$applying_guidelines = NULL;
$order_items = [];
foreach ($order->getItems() as $order_item) {
$title = $order_item->get('title')->value;
$sku = substr($title, strpos($title, ":") + 1);
$shipping = $this->entityTypeManager->getStorage('commerce_shipment')
->load($order->get('shipments')[0]->target_id);
// Get asin from product variation.
$product = $this->entityTypeManager->getStorage('commerce_product')->load($order_item->getPurchasedEntityId());
if ($product == NULL) {
continue;
}
$order_items[] = [
'quantity' => $order_item->get('quantity')->value,
'title' => $title,
'price' => $order_item->get('total_price')->number,
'sku' => $sku,
'shipping' => $shipping->get('amount')[0]->number,
];
}
$save_order = [
'delivery_fullname' => $order->getBillingProfile()->get('address')[0]->given_name . ' ' . $order->getBillingProfile()->get('address')->family_name,
'delivery_address' => $order->getBillingProfile()->get('address')->address_line1,
'delivery_postal_code' => $order->getBillingProfile()->get('address')->postal_code,
'delivery_city' => $order->getBillingProfile()->get('address')[0]->locality,
'delivery_country' => $order->getBillingProfile()->get('address')[0]->country_code,
'order_number' => $order->getOrderNumber(),
'date_add' => date('D., d.M.Y', $order->get('created')->value),
'order_items' => $order_items,
];
$currency = $this->entityTypeManager->getStorage('commerce_currency')->load($order->getTotalPrice()->getCurrencyCode());
$build = [
'#theme' => 'belmilb2b_baselinker_pdf',
'#order_total_paid' => $order->getTotalPrice()->getNumber(),
'#pdf_title' => $pdf_title,
'#title' => 'test',
'#applying_guidelines' => $applying_guidelines,
'#order' => $save_order,
'#attached' => [
'library' => $theme_info['libraries'],
],
'#currency' => $currency->getSymbol(),
];
$logo = $this->themeInitialization->getActiveThemeByName($theme)
->getLogo();
if (strpos($logo, '.svg')) {
$logo = str_replace('.svg', '.png', $logo);
}
$build['#site_logo'] = $logo;
$context = new RenderContext();
$css_assets = $this->assetResolver->getCssAssets(AttachedAssets::createFromRenderArray($build), TRUE);
$rendered_css = $this->cssCollectionRenderer->render($css_assets);
/** @var \Drupal\Core\Cache\CacheableDependencyInterface $rendered_css_build */
$rendered_css_build = $this->renderer->executeInRenderContext($context, function () use ($rendered_css, $renderer) {
return $renderer->render($rendered_css);
});
if (!$context->isEmpty()) {
$bubbleable_metadata = $context->pop();
BubbleableMetadata::createFromObject($rendered_css_build)
->merge($bubbleable_metadata);
}
$build['#rendered_css'] = $rendered_css_build;
/** @var \Drupal\Core\Cache\CacheableDependencyInterface $html */
$html = $this->renderer->executeInRenderContext($context, function () use ($build, $renderer) {
return $renderer->render($build);
});
if (!$context->isEmpty()) {
$bubbleable_metadata = $context->pop();
BubbleableMetadata::createFromObject($html)
->merge($bubbleable_metadata);
}
$host = $this->request->getSchemeAndHttpHost();
if (!$is_html && strpos($host, 'docker') !== FALSE) {
$host = 'http://nginx';
}
$media_string = '<link rel="stylesheet" media="all" href=""';
$css_string = '<link rel="stylesheet" type="text/css" href=""';
$html = str_replace('<img src=""', '<img src=""' . $host, $html);
$html = str_replace($media_string, $media_string . $host, $html);
$html = str_replace($css_string, $css_string . $host, $html);
if (!$is_html) {
$destination_dir = 'public://baselinker_order_pdf';
$destination = $destination_dir . '/baselinker-pdf- ' . $save_order['order_number'] . '.pdf';
$this->fileSystem->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY || FileSystemInterface::MODIFY_PERMISSIONS);
$pdf_data = $this->httpClient
->request('POST', $this->athenapdfConfig->get('endpoint') . '?auth=' . $this->athenapdfConfig->get('auth_key') . '&ext=html', [
RequestOptions::MULTIPART => [
[
'name' => 'file',
'contents' => $html,
'filename' => 'input.html',
'headers' => [
'Content-Type' => 'text/html; charset=utf-8',
],
],
],
RequestOptions::SINK => $destination,
])->getBody()->getContents();
$pdf_file = $this->fileSystem
->saveData($pdf_data, $destination, FileSystemInterface::EXISTS_REPLACE);
$order->field_documents->target_id = $pdf_file;
$order->save();
if ($return_with_file) {
return $pdf_file ? $this->fileUrlGenerator->generateAbsoluteString($pdf_file) : NULL;
}
else {
$this->setPDFTitle($destination, $pdf_title);
$response = new Response();
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set('Content-Disposition', 'inline; filename="' . $pdf_title . '.pdf"');
$response->setContent($pdf_data);
return $response;
}
}
else {
return new Response($html);
}
throw new NotFoundHttpException();
}
/**
* Set PDF title.
*
* @param string $outputFilePath
* Output file path.
* @param string $title
* Title.
*/
protected function setPdfTitle(string $outputFilePath, string $title) {
try {
$mpdf = new Mpdf(['tempDir' => $this->fileSystem->getTempDirectory()]);
$page_count = $mpdf->setSourceFile($outputFilePath);
for ($i = 1; $i <= $page_count; $i++) {
$import_page = $mpdf->ImportPage($i);
$mpdf->UseTemplate($import_page);
if ($i < $page_count) {
$mpdf->AddPage();
}
}
$mpdf->setTitle($title);
$mpdf->Output($outputFilePath, Destination::FILE);
}
catch (\Exception $e) {
// Do nothing.
}
}
}