434 lines
15 KiB
PHP
434 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KupShop\PreordersBundle\Service;
|
|
|
|
use Doctrine\DBAL\Exception;
|
|
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
|
use KupShop\CatalogBundle\ProductList\ProductList;
|
|
use KupShop\I18nBundle\Entity\Currency;
|
|
use KupShop\I18nBundle\Util\PriceConverter;
|
|
use KupShop\KupShopBundle\Context\CurrencyContext;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Price\Price;
|
|
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
|
|
use KupShop\KupShopBundle\Util\Price\ProductPrice;
|
|
use KupShop\KupShopBundle\Util\Price\TotalPrice;
|
|
use KupShop\KupShopBundle\Wrapper\PriceWrapper;
|
|
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
|
|
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
|
|
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
|
|
use KupShop\PreordersBundle\Exception\UnsupportedOperationException;
|
|
use KupShop\PreordersBundle\Translations\PreordersDatesTranslation;
|
|
use KupShop\PreordersBundle\Translations\PreordersTranslation;
|
|
use Query\Operator;
|
|
use Query\Operator as Op;
|
|
use Query\Product;
|
|
use Query\QueryBuilder;
|
|
use Query\Translation;
|
|
|
|
class PreorderUtil
|
|
{
|
|
public const PREORDER_BASE_PRICE_FIELD = 'preorder_base_price_field';
|
|
|
|
public function __construct(
|
|
private readonly CurrencyContext $currencyContext,
|
|
private readonly PurchaseUtil $purchaseUtil,
|
|
private readonly PriceConverter $priceConverter,
|
|
) {
|
|
}
|
|
|
|
public function getPreorderProductList(?array $items = null, bool $variationsAsResult = true): ProductList
|
|
{
|
|
$productList = (new ProductList())
|
|
->fetchVariations(true, false)
|
|
->setVariationsAsResult($variationsAsResult);
|
|
|
|
if ($items !== null) {
|
|
// filter out unwanted products
|
|
$productList->andSpec(self::specSelectProductAndVariationIds($items));
|
|
|
|
// inject pre-ordered pieces into Product objects
|
|
$productList->addResultModifiers(self::modifierInjectFields($items, ['pieces' => 'pieces']));
|
|
|
|
// inject piece_price from DB -> used when creating an order - keep the same price
|
|
// when the item was initially preordered
|
|
$productList->addResultModifiers($this->modifierInjectPiecePrices($items, $variationsAsResult));
|
|
}
|
|
|
|
return $productList;
|
|
}
|
|
|
|
public function modifierProductPriceToActiveCurrency(): callable
|
|
{
|
|
$activeCurrency = Contexts::get(CurrencyContext::class)->getActive();
|
|
|
|
return function (ProductCollection $products) use ($activeCurrency) {
|
|
foreach ($products as $product) {
|
|
$productPrice = $product->getProductPrice();
|
|
|
|
if ($productPrice->getCurrency()->getId() === $activeCurrency->getId()) {
|
|
continue;
|
|
}
|
|
|
|
$convertedPrice = $this->priceConverter->convertPrice(
|
|
fromCurrency: $productPrice->getCurrency(),
|
|
toCurrency: $activeCurrency,
|
|
price: $productPrice->getValue(),
|
|
);
|
|
|
|
$product->setProductPrice(new ProductPrice($convertedPrice, $activeCurrency, $productPrice->getVat()));
|
|
}
|
|
};
|
|
}
|
|
|
|
public function modifierInjectPiecePrices(
|
|
array $itemsWithPiecePrices,
|
|
bool $variationsAsResult = true,
|
|
string $inKey = 'piece_price',
|
|
string $outKey = 'preorder_piece_price',
|
|
): callable {
|
|
if ($variationsAsResult) {
|
|
return function (ProductCollection $products) use ($itemsWithPiecePrices, $inKey, $outKey) {
|
|
foreach ($itemsWithPiecePrices as $itemWithPrice) {
|
|
if (empty($itemWithPrice[$inKey])) {
|
|
continue;
|
|
}
|
|
|
|
if (!empty($itemWithPrice['id_variation'])) {
|
|
$product = $products[$itemWithPrice['id_product'].'/'.$itemWithPrice['id_variation']];
|
|
} else {
|
|
$product = $products[$itemWithPrice['id_product']] ?? null;
|
|
|
|
if (!$product) {
|
|
foreach ($products as $maybeProduct) {
|
|
if ($maybeProduct->id === $itemWithPrice['id_product']) {
|
|
$product = $maybeProduct;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$product) {
|
|
continue;
|
|
}
|
|
|
|
$price = $product->getProductPrice();
|
|
$currency = $this->getCurrency($itemWithPrice['currency'] ?? null);
|
|
|
|
$product[$outKey] = new Price(
|
|
toDecimal($itemWithPrice[$inKey]),
|
|
$currency ?? $price->getCurrency(),
|
|
$price->getVat(),
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
return function (ProductCollection $products) use ($itemsWithPiecePrices, $inKey, $outKey) {
|
|
foreach ($itemsWithPiecePrices as $item) {
|
|
if (empty($item[$inKey])) {
|
|
continue;
|
|
}
|
|
|
|
if (!empty($item['id_variation'])) {
|
|
$price = $products[$item['id_product']]->variations[$item['id_variation']]['productPrice'];
|
|
$currency = $this->getCurrency($item['currency'] ?? null);
|
|
|
|
$products[$item['id_product']]->variations[$item['id_variation']][$outKey] = new Price(
|
|
toDecimal($item[$inKey]),
|
|
$currency ?? $price->getCurrency(),
|
|
$price->getVat(),
|
|
);
|
|
} else {
|
|
$price = $products[$item['id_product']]->getProductPrice();
|
|
$currency = $this->getCurrency($item['currency'] ?? null);
|
|
|
|
$products[$item['id_product']][$outKey] = new Price(
|
|
toDecimal($item[$inKey]),
|
|
$currency ?? $price->getCurrency(),
|
|
$price->getVat(),
|
|
);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private function getCurrency(?string $id): ?Currency
|
|
{
|
|
$currencies = $this->currencyContext->getAll();
|
|
|
|
if ($id && isset($currencies[$id])) {
|
|
return $currencies[$id];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function calculateBasePrice(ProductCollection $products): TotalPrice
|
|
{
|
|
$purchaseState = self::toPurchaseState($products, self::PREORDER_BASE_PRICE_FIELD);
|
|
$this->purchaseUtil->recalculateTotalPrices($purchaseState);
|
|
|
|
return $purchaseState->getTotalPrice();
|
|
}
|
|
|
|
public static function toPurchaseState(ProductCollection $products, ?string $customPriceField = null): PurchaseState
|
|
{
|
|
$purchaseItems = [];
|
|
|
|
/** @var \Product $product */
|
|
foreach ($products as $product) {
|
|
if ($product instanceof \Variation || empty($product->variations)) {
|
|
if (empty($product['pieces'])) {
|
|
continue;
|
|
}
|
|
|
|
$price = match ($customPriceField) {
|
|
null => $product->getProductPrice(),
|
|
self::PREORDER_BASE_PRICE_FIELD => $product['dynamic_prices'][0]['price'],
|
|
default => $product[$customPriceField] ?? null,
|
|
};
|
|
|
|
if (null === $price) {
|
|
throw new UnsupportedOperationException('Custom price field is empty!');
|
|
}
|
|
|
|
if ($price instanceof PriceWrapper) {
|
|
$price = $price->getObject();
|
|
}
|
|
|
|
if ($price instanceof ProductPrice && !empty($price->getDiscount())) {
|
|
$price = PriceCalculator::addDiscount($price, toDecimal($price->getDiscount()));
|
|
}
|
|
|
|
$purchaseItems[] = new ProductPurchaseItem(
|
|
$product->id,
|
|
($product instanceof \Variation)
|
|
? $product->variationId : null,
|
|
(int) $product['pieces'],
|
|
PriceCalculator::mul($price, toDecimal($product['pieces'])),
|
|
null
|
|
);
|
|
continue;
|
|
}
|
|
|
|
foreach ($product->variations as $variation) {
|
|
if (empty($variation['pieces'])) {
|
|
continue;
|
|
}
|
|
|
|
$price = match ($customPriceField) {
|
|
null => $variation['productPrice'],
|
|
self::PREORDER_BASE_PRICE_FIELD => $variation['dynamic_prices'][0]['price'],
|
|
default => $variation[$customPriceField] ?? null,
|
|
};
|
|
|
|
if (null === $price) {
|
|
throw new UnsupportedOperationException('Custom price field is empty!');
|
|
}
|
|
|
|
if ($price instanceof PriceWrapper) {
|
|
$price = $price->getObject();
|
|
}
|
|
|
|
$purchaseItems[] = new ProductPurchaseItem(
|
|
$product->id,
|
|
$variation['id'],
|
|
(int) $variation['pieces'],
|
|
PriceCalculator::mul($price, toDecimal($variation['pieces'])),
|
|
null
|
|
);
|
|
}
|
|
}
|
|
|
|
return new PurchaseState($purchaseItems);
|
|
}
|
|
|
|
public static function injectFields(ProductCollection $products, array $itemsWithValues, array $fieldMapping = []): void
|
|
{
|
|
foreach ($itemsWithValues as $item) {
|
|
$toInject = [];
|
|
foreach ($fieldMapping as $formerKey => $newKey) {
|
|
$toInject[$newKey] = $item[$formerKey];
|
|
}
|
|
|
|
$itemIsVariation = isset($item['id_variation']);
|
|
|
|
$isClassVariationInstance = $itemIsVariation
|
|
&& isset($products[$item['id_product'].'/'.$item['id_variation']]);
|
|
|
|
if ($isClassVariationInstance) {
|
|
$object = $products[$item['id_product'].'/'.$item['id_variation']];
|
|
|
|
foreach ($toInject as $key => $val) {
|
|
$object[$key] = $val;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!isset($products[$item['id_product']])) {
|
|
// Product now has variations, but was saved without -> try to find it...
|
|
$product = null;
|
|
foreach ($products as $maybeProduct) {
|
|
if ($maybeProduct->id === (int) $item['id_product']) {
|
|
$product = $maybeProduct;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($product) {
|
|
foreach ($toInject as $key => $value) {
|
|
$product[$key] = $value;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($itemIsVariation) {
|
|
foreach ($toInject as $key => $value) {
|
|
$products[$item['id_product']]->variations[$item['id_variation']][$key] = $value;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// item is a product without variations
|
|
foreach ($toInject as $key => $val) {
|
|
$products[$item['id_product']][$key] = $val;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function modifierInjectFields(array $itemsWithValues, array $fieldMapping = []): callable
|
|
{
|
|
return function (ProductCollection $products) use ($itemsWithValues, $fieldMapping) {
|
|
self::injectFields($products, $itemsWithValues, $fieldMapping);
|
|
};
|
|
}
|
|
|
|
public static function specSelectProductAndVariationIds(array $items): callable
|
|
{
|
|
$ids = [];
|
|
foreach ($items as $item) {
|
|
if (!array_key_exists($item['id_product'], $ids)) {
|
|
$ids[$item['id_product']] = [];
|
|
}
|
|
if (!empty($item['id_variation'])) {
|
|
$ids[$item['id_product']][] = $item['id_variation'];
|
|
}
|
|
}
|
|
|
|
foreach ($ids as $productId => $variationId) {
|
|
if (empty($variationId)) {
|
|
$ids[$productId] = null;
|
|
}
|
|
}
|
|
|
|
return Product::productsAndVariationsIds($ids);
|
|
}
|
|
|
|
public static function valueIsComposedOfDigits($value): bool
|
|
{
|
|
return is_int($value) || (is_string($value) && ctype_digit($value));
|
|
}
|
|
|
|
public static function fieldIsComposedOfDigits($array, $key): bool
|
|
{
|
|
if (!is_array($array) && !($array instanceof \ArrayAccess)) {
|
|
return false;
|
|
}
|
|
|
|
return isset($array[$key]) && self::valueIsComposedOfDigits($array[$key]);
|
|
}
|
|
|
|
public static function specDateIsOpen(): callable
|
|
{
|
|
return function (QueryBuilder $qb) {
|
|
$qb->andWhere('pd.date_start <= NOW()')
|
|
->andWhere('pd.date_end >= CURDATE()');
|
|
};
|
|
}
|
|
|
|
public static function useMultipleDates(): bool
|
|
{
|
|
$dbcfg = \Settings::getDefault();
|
|
|
|
return ($dbcfg->preorders['multiple_dates'] ?? 'N') === 'Y';
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
* @throws \Doctrine\DBAL\Driver\Exception
|
|
*/
|
|
public static function getOpenDates(?\User $user, ?array $preorderIds = null): array
|
|
{
|
|
$preorders = sqlQueryBuilder()
|
|
->select('po.*')
|
|
->from('preorders', 'po')
|
|
->andWhere(Translation::coalesceTranslatedFields(PreordersTranslation::class, ['name' => 'preorder_name']));
|
|
|
|
$datesQb = sqlQueryBuilder()
|
|
->select('pd.*', 'po.*', 'pd.id AS id', 'po.preorder_name AS name')
|
|
->from('preorders_dates', 'pd')
|
|
->leftJoinSubQuery('pd', $preorders, 'po', 'pd.id_preorder = po.id')
|
|
->andWhere(PreorderUtil::specDateIsOpen())
|
|
->andWhere(Translation::coalesceTranslatedFields(PreordersDatesTranslation::class))
|
|
->orderBy('pd.date_end', 'ASC');
|
|
|
|
if ($preorderIds !== null) {
|
|
$datesQb->andWhere(Op::inIntArray($preorderIds, 'pd.id_preorder'));
|
|
}
|
|
|
|
$dates = $datesQb->execute()->fetchAllAssociative();
|
|
|
|
if ($user) {
|
|
foreach ($dates as $key => &$date) {
|
|
if (is_string($date['settings'])) {
|
|
$date['settings'] = json_decode($date['settings'], true);
|
|
}
|
|
|
|
$groups = $user->getGroups();
|
|
$hasGroup = false;
|
|
|
|
foreach (($date['settings']['users_groups'] ?? []) as $group) {
|
|
if (array_key_exists((int) $group, $groups)) {
|
|
$hasGroup = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasGroup) {
|
|
unset($dates[$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values($dates);
|
|
}
|
|
|
|
public static function countUserPreorders(\User|int $user, ?callable $spec = null): int
|
|
{
|
|
if ($user instanceof \User) {
|
|
$user = $user->id;
|
|
}
|
|
|
|
$countQuery = sqlQueryBuilder()
|
|
->select('COUNT(DISTINCT pi.id_preorder_date)')
|
|
->from('preorders_items', 'pi')
|
|
->andWhere(Operator::equals(['pi.id_user' => $user]));
|
|
|
|
if ($spec) {
|
|
$countQuery->andWhere($spec);
|
|
}
|
|
|
|
return (int) $countQuery->execute()->fetchOne();
|
|
}
|
|
}
|