191 lines
8.8 KiB
PHP
191 lines
8.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\ProductInquiry\Model;
|
|
|
|
use Esun\ProductInquiry\Model\ResourceModel\Inquiry as InquiryResource;
|
|
use Esun\ProductInquiry\Model\ResourceModel\Inquiry\CollectionFactory;
|
|
use Magento\Catalog\Api\ProductRepositoryInterface;
|
|
use Magento\Catalog\Model\Product;
|
|
use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
|
|
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
|
|
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable as ConfigurableResource;
|
|
use Magento\Customer\Model\Session as CustomerSession;
|
|
use Magento\Directory\Model\CountryFactory;
|
|
use Magento\Framework\App\RequestInterface;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress;
|
|
use Magento\Framework\Serialize\Serializer\Json;
|
|
use Magento\Framework\Stdlib\DateTime\DateTime;
|
|
use Magento\Store\Model\StoreManagerInterface;
|
|
|
|
class SubmissionService
|
|
{
|
|
private const MAX_DAILY_SUBMISSIONS = 3;
|
|
private const MAX_DAILY_IP_SUBMISSIONS = 20;
|
|
|
|
public function __construct(
|
|
private readonly ProductRepositoryInterface $productRepository,
|
|
private readonly ConfigurableResource $configurableResource,
|
|
private readonly AvailabilityResolver $availabilityResolver,
|
|
private readonly CountryFactory $countryFactory,
|
|
private readonly InquiryFactory $inquiryFactory,
|
|
private readonly InquiryResource $inquiryResource,
|
|
private readonly CollectionFactory $collectionFactory,
|
|
private readonly StoreManagerInterface $storeManager,
|
|
private readonly CustomerSession $customerSession,
|
|
private readonly RemoteAddress $remoteAddress,
|
|
private readonly DateTime $dateTime,
|
|
private readonly Json $json
|
|
) {
|
|
}
|
|
|
|
public function submit(RequestInterface $request): array
|
|
{
|
|
$data = $this->validateContactData($request);
|
|
$store = $this->storeManager->getStore();
|
|
$website = $store->getWebsite();
|
|
$parent = $this->productRepository->getById((int)$request->getParam('parent_product_id'), false, (int)$store->getId());
|
|
$product = $this->productRepository->getById((int)$request->getParam('product_id'), false, (int)$store->getId());
|
|
|
|
if (!$parent instanceof Product || $parent->getTypeId() !== Configurable::TYPE_CODE) {
|
|
throw new LocalizedException(__('The inquiry product is invalid.'));
|
|
}
|
|
if (!$product instanceof Product || (int)$product->getStatus() !== ProductStatus::STATUS_ENABLED) {
|
|
throw new LocalizedException(__('The selected product is unavailable.'));
|
|
}
|
|
if (!in_array((int)$parent->getId(), array_map('intval', $this->configurableResource->getParentIdsByChild((int)$product->getId())), true)) {
|
|
throw new LocalizedException(__('The selected product does not belong to this product.'));
|
|
}
|
|
|
|
$availability = $this->availabilityResolver->resolve($product, $website);
|
|
if (!$availability['requires_inquiry']) {
|
|
throw new LocalizedException(__('This product is currently available for purchase.'));
|
|
}
|
|
|
|
$messageHash = hash('sha256', $data['email'] . '|' . $product->getId() . '|' . $this->normalizeMessage($data['message']));
|
|
$remoteIpHash = $this->getRemoteIpHash();
|
|
if ($this->isExactDuplicate($messageHash)) {
|
|
return ['duplicate' => true];
|
|
}
|
|
$this->assertWithinRateLimit($data['email'], $remoteIpHash);
|
|
|
|
$country = $this->countryFactory->create()->loadByCode($data['country_id']);
|
|
if (!$country->getId()) {
|
|
throw new LocalizedException(__('Select a valid country.'));
|
|
}
|
|
|
|
$inquiry = $this->inquiryFactory->create();
|
|
$inquiry->setData([
|
|
'parent_product_id' => (int)$parent->getId(),
|
|
'parent_sku' => (string)$parent->getSku(),
|
|
'parent_name' => (string)$parent->getName(),
|
|
'product_id' => (int)$product->getId(),
|
|
'product_sku' => (string)$product->getSku(),
|
|
'product_name' => (string)$product->getName(),
|
|
'selected_options' => $this->json->serialize($this->getSelectedOptions($parent, $product)),
|
|
'store_id' => (int)$store->getId(),
|
|
'website_id' => (int)$website->getId(),
|
|
'stock_id' => (int)$availability['stock_id'],
|
|
'customer_id' => $this->customerSession->isLoggedIn() ? (int)$this->customerSession->getCustomerId() : null,
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'country_id' => (string)$country->getId(),
|
|
'country_name' => (string)$country->getName(),
|
|
'company' => $data['company'],
|
|
'telephone' => $data['telephone'],
|
|
'message' => $data['message'],
|
|
'status' => Inquiry::STATUS_NEW,
|
|
'stock_status' => (int)$availability['stock_status'],
|
|
'is_salable' => (int)$availability['is_salable'],
|
|
'availability_reason' => (string)$availability['reason'],
|
|
'message_hash' => $messageHash,
|
|
'remote_ip_hash' => $remoteIpHash,
|
|
]);
|
|
$this->inquiryResource->save($inquiry);
|
|
|
|
return ['duplicate' => false, 'inquiry_id' => (int)$inquiry->getId()];
|
|
}
|
|
|
|
private function validateContactData(RequestInterface $request): array
|
|
{
|
|
if (trim((string)$request->getParam('hideit', '')) !== '') {
|
|
throw new LocalizedException(__('Unable to submit the form.'));
|
|
}
|
|
|
|
$data = [
|
|
'name' => trim((string)$request->getParam('name', '')),
|
|
'email' => mb_strtolower(trim((string)$request->getParam('email', ''))),
|
|
'country_id' => strtoupper(trim((string)$request->getParam('country_id', ''))),
|
|
'company' => trim((string)$request->getParam('company', '')),
|
|
'telephone' => trim((string)$request->getParam('telephone', '')),
|
|
'message' => trim((string)$request->getParam('message', '')),
|
|
];
|
|
|
|
if ($data['name'] === '' || $data['message'] === '' || $data['country_id'] === '') {
|
|
throw new LocalizedException(__('Complete all required fields.'));
|
|
}
|
|
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
|
throw new LocalizedException(__('Enter a valid email address.'));
|
|
}
|
|
if (mb_strlen($data['name']) > 255 || mb_strlen($data['email']) > 255 || mb_strlen($data['company']) > 255 || mb_strlen($data['telephone']) > 64) {
|
|
throw new LocalizedException(__('One or more fields are too long.'));
|
|
}
|
|
if (mb_strlen($data['message']) > 5000 || strlen($data['country_id']) !== 2) {
|
|
throw new LocalizedException(__('The submitted information is invalid.'));
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function getSelectedOptions(Product $parent, Product $product): array
|
|
{
|
|
$options = [];
|
|
foreach ($parent->getTypeInstance()->getConfigurableAttributes($parent) as $configurableAttribute) {
|
|
$attribute = $configurableAttribute->getProductAttribute();
|
|
$value = $product->getAttributeText((string)$attribute->getAttributeCode());
|
|
$options[] = [
|
|
'label' => (string)$attribute->getStoreLabel(),
|
|
'value' => is_array($value) ? implode(', ', $value) : (string)$value,
|
|
];
|
|
}
|
|
return $options;
|
|
}
|
|
|
|
private function isExactDuplicate(string $messageHash): bool
|
|
{
|
|
return $this->collectionFactory->create()
|
|
->addFieldToFilter('message_hash', $messageHash)
|
|
->addFieldToFilter('created_at', ['gteq' => $this->dateTime->gmtDate('Y-m-d H:i:s', time() - 600)])
|
|
->getSize() > 0;
|
|
}
|
|
|
|
private function assertWithinRateLimit(string $email, string $remoteIpHash): void
|
|
{
|
|
$since = $this->dateTime->gmtDate('Y-m-d H:i:s', time() - 86400);
|
|
$emailCount = $this->collectionFactory->create()
|
|
->addFieldToFilter('email', $email)
|
|
->addFieldToFilter('created_at', ['gteq' => $since])
|
|
->getSize();
|
|
$ipCount = $remoteIpHash === '' ? 0 : $this->collectionFactory->create()
|
|
->addFieldToFilter('remote_ip_hash', $remoteIpHash)
|
|
->addFieldToFilter('created_at', ['gteq' => $since])
|
|
->getSize();
|
|
|
|
if ($emailCount >= self::MAX_DAILY_SUBMISSIONS || $ipCount >= self::MAX_DAILY_IP_SUBMISSIONS) {
|
|
throw new LocalizedException(__('Too many inquiries have been submitted. Please try again later.'));
|
|
}
|
|
}
|
|
|
|
private function normalizeMessage(string $message): string
|
|
{
|
|
return mb_strtolower((string)preg_replace('/\s+/u', ' ', trim($message)));
|
|
}
|
|
|
|
private function getRemoteIpHash(): string
|
|
{
|
|
$ip = trim((string)$this->remoteAddress->getRemoteAddress());
|
|
return $ip === '' ? '' : hash('sha256', $ip);
|
|
}
|
|
}
|