Files
kupshop/class/class.Cart.php
2025-08-02 16:30:27 +02:00

2469 lines
84 KiB
PHP

<?php
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\ContentBundle\Exception\InvalidCartItemException;
use KupShop\ContentBundle\Util\CartMerge;
use KupShop\I18nBundle\Translations\ProductsTranslation;
use KupShop\I18nBundle\Translations\VariationsTranslation;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Event\ErrorIgnoringEventDispatcher;
use KupShop\KupShopBundle\Util\Mail\EmailCheck;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Wrapper\PriceWrapper;
use KupShop\OrderDiscountBundle\Util\DiscountManager;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\OrderingBundle\Event\CartBeforeInsertItemEvent;
use KupShop\OrderingBundle\Event\CartEvent;
use KupShop\OrderingBundle\Event\CartItemEvent;
use KupShop\OrderingBundle\Event\OrderEvent;
use KupShop\OrderingBundle\Event\PurchaseStateCreatedEvent;
use KupShop\OrderingBundle\Exception\CartValidationException;
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
use KupShop\OrderingBundle\Wrapper\Purchase\ProductPurchaseItemWrapper;
use Query\Operator;
use Query\QueryBuilder;
use Query\Translation;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Service\Attribute\Required;
/**
* Class for Order preparation.
*
* @author Joe
*/
class CartBase implements ArrayAccess
{
use DatabaseCommunication;
public $products = [];
/** @var ProductCollection */
public $productList;
public $user;
/** @var Decimal */
public $totalPriceNoVat;
/** @var Decimal */
public $totalPriceWithVat;
/** @var Decimal */
public $totalPricePay;
/** @var Decimal */
public $totalPricePayNoVat;
/**
* @var Decimal[]
*/
public $priceByVat = [];
public $totalPieces = 0.;
/**
* @var Decimal
*/
public $totalDiscountPrice;
/**
* @var Decimal
*/
public $totalDiscountPriceNoVat;
/**
* @var Decimal
*/
public $totalChargesPrice;
/**
* @var Decimal
*/
public $totalChargesPriceNoVat;
public $freeShippingValue = 0.;
public $freeShipping = false;
public $openOrders = [];
public $vats = [];
public $orders_charges;
public $charges;
/** @var DeliveryTypeBase[] */
public $delivery_types = [];
public $virtual_products;
public $only_virtual_products;
public $coupons = [];
// User info
public $invoice = [];
public $delivery = [];
public $transport;
public $delivery_id;
public $payment_id;
public $note;
public $newsletter = false;
public $addressesSet = false;
public $userOrderNo;
public $paymentData = [];
public $deliveryData = [];
public $cart_data;
/**
* @var DeliveryType
*/
protected $deliveryType;
protected $storeDeliveryData = false;
public $register = false;
public $max_step = 0;
public $max_step_submitted = -1;
protected $errors = [];
public static $NO_CART_ID = 'no-cartID';
public static $ERR_NOT_SELECTED_DELIVERY = 5;
public static $ERR_NOT_SUPPORTED_COUNTRY = 18;
public static $ERR_EXISTENT_PRODUCT = 18;
public static $ERR_INVALID_VARIATION = 19;
public static $ERR_OUT_OF_STOCK = 30;
public static $ERR_NOT_SELECTED_VARIATION = 31;
public static $ERR_CANNOT_BE_PURCHASED = 33;
private SessionInterface $session;
/** @var PurchaseUtil */
protected $purchaseUtil;
/** @var PurchaseState */
protected $purchaseState;
protected ?PurchaseState $previousPurchaseState = null;
protected ?RequestStack $requestStack = null;
public $actualStep;
private static $cartID;
/**
* @var array
*/
public $steps;
/**
* @var bool
*/
public $selected;
private $actionHandlersData = [];
private $initialized = false;
/**
* Constructor.
*/
public function __construct()
{
global $cfg;
$this->session = ServiceContainer::getService('session');
if (empty($cfg['Order']['Steps'])) {
// die("Nejsou nadefinovány kroky objenávky");
// Kroky objednavkoveho procesu
$this->steps = [
'cart' => [
'title' => translate('step_cart', 'cart'),
'name' => '',
'data' => ['items'],
],
'delivery' => [
'title' => translate('step_delivery', 'cart'),
'name' => translate('url_delivery', 'cart'),
'data' => ['delivery', 'payment'],
],
'user' => [
'title' => translate('step_user', 'cart'),
'name' => translate('url_user', 'cart'),
'data' => ['user'],
],
'summary' => [
'title' => translate('step_summary', 'cart'),
'name' => translate('url_summary', 'cart'),
],
];
} else {
$this->steps = $cfg['Order']['Steps'];
foreach ($this->steps as &$step) {
if (!empty($step['title_translate'])) {
$step['title'] = translate($step['title_translate'], 'cart');
}
if (!empty($step['name_translate'])) {
$step['name'] = translate($step['name_translate'], 'cart');
}
}
}
foreach ($this->steps as $stepName => &$step) {
if (empty($step['template'])) {
$step['template'] = "ordering.{$stepName}.tpl";
}
if (!isset($step['name'])) {
$step['name'] = $stepName;
}
if (empty($step['url'])) {
if (empty($step['name'])) {
$step['url'] = path('kupshop_content_cart_cart_1');
} else {
$step['url'] = path('kupshop_content_cart_cart', ['step' => $step['name']]);
}
// backward compatibility
$step['url'] = ltrim($step['url'], '/');
}
}
$this->user = new User();
$this->invoice = &$this->user->invoice;
$this->delivery = &$this->user->delivery;
$this->transport = &$this->user->transport;
$this->totalDiscountPrice = DecimalConstants::zero();
$this->totalDiscountPriceNoVat = DecimalConstants::zero();
$this->totalPricePay = DecimalConstants::zero();
$this->totalPricePayNoVat = DecimalConstants::zero();
$this->totalPriceNoVat = DecimalConstants::zero();
$this->totalPriceWithVat = DecimalConstants::zero();
$this->totalChargesPrice = DecimalConstants::zero();
$this->totalChargesPriceNoVat = DecimalConstants::zero();
// prepare productList
$this->productList = new ProductCollection();
$this->productList->setEntityType(FilterParams::ENTITY_VARIATION);
}
#[Required]
final public function setPurchaseUtil(PurchaseUtil $purchaseUtil): void
{
$this->purchaseUtil = $purchaseUtil;
}
public function hasData($type, $submitted = true)
{
$i = 0;
$maxStep = $this->max_step_submitted;
if (!$submitted) {
$maxStep = $this->max_step;
}
foreach ($this->steps as $step) {
if ($i > $maxStep) {
break;
}
if (in_array($type, getVal('data', $step, []))) {
return true;
}
$i++;
}
return false;
}
public function findStep(&$name)
{
$step = &$this->steps[$name];
$this->actualStep = $name;
$step['selected'] = true;
return $step;
}
public function findNextStep($name)
{
$steps = array_keys($this->steps);
$index = array_search($name, $steps) + 1;
$this->max_step = $index;
return $this->findStep($steps[$index]);
}
public function findPreviousStep($name)
{
$steps = array_keys($this->steps);
$index = array_search($name, $steps) - 1;
if ($index < 0) {
return false;
}
return $this->findStep($steps[$index]);
}
public function visitStep(&$name, $submitted, $changed)
{
if (empty($this->steps[$name])) {
foreach ($this->steps as $stepName => &$step) {
if ($step['name'] == $name) {
$name = $stepName;
break;
}
}
}
$steps = array_keys($this->steps);
if (empty($this->steps[$name])) {
return $steps[0];
}
$index = array_search($name, $steps);
if ($index > $this->max_step) {
return $steps[$this->max_step];
} elseif ($index < $this->max_step && $changed) {
$this->max_step = $index;
$this->max_step_submitted = $index - 1;
}
if ($submitted) {
$this->max_step_submitted = $this->max_step;
}
return false;
}
/**
* Fetches data from database, product ID specified in $pid.
*
* @return bool
*/
public function createFromDB()
{
if ($this->initialized) {
return;
}
$this->initialized = true;
// Get all products
$this->fetchProducts();
$this->fetchCharges();
// Get all discounts
$this->fetchDiscounts();
// Fetch vats
$this->fetchVats();
// Fetch address from user
$this->fetchAddresses();
// Fetch totals
$this->fetchTotals();
// Fetch delivery types
$this->fetchDelivery();
// cenu dopravy nechceme pricitat do celkove ceny na prvnim kroku kosiku
if ($this->transport && $this->getActualStepIndex() > 0) {
$purchaseState = $this->getPurchaseState();
// nasetovat delivery type do purchase statu, aby se mohla pricist cena dopravy k celkove cene
$purchaseState->setDeliveryType($this->getDeliveryType());
$this->purchaseUtil->recalculateTotalPrices($purchaseState);
$this->purchaseUtil->ensureTotalIsNotNegative($purchaseState);
$this->fetchTotalsFromPurchaseState($purchaseState);
}
// Otevrene objednavky uzivatele
$this->openOrders = Order::getOpenOrders();
$this->cart_data = $this->getData();
}
public function setActionHandlersData(array $actionHandlersData)
{
$this->actionHandlersData = $actionHandlersData;
}
public function getActionHandlersData(): array
{
return $this->actionHandlersData;
}
public function getPurchaseState(): PurchaseState
{
if (isset($this->purchaseState)) {
return $this->purchaseState;
}
if (self::getCartID() === self::$NO_CART_ID) {
$purchaseState = $this->purchaseUtil->getEmptyPurchaseState();
} else {
// get and remove whether current purchase state was invalidated
$purchaseStateIsInvalid = $this->session->remove('cart.purchase_state.invalid');
if (($purchaseState = $this->purchaseUtil->unserializeState($this->session->get('cart.purchase_state'))) && $purchaseStateIsInvalid) {
$this->previousPurchaseState = $purchaseState;
$purchaseState = null;
}
}
if (!$purchaseState) {
$this->createFromDB();
$purchaseState = $this->purchaseUtil->createStateFromCart($this);
// add order discounts to PurchaseState
if (findModule(Modules::ORDER_DISCOUNT)) {
$discountManager = ServiceContainer::getService(DiscountManager::class);
$discountManager->setPurchaseState($purchaseState);
if ($purchaseState->getProducts()) {
$discountManager->setActionHandlersData($this->actionHandlersData);
$purchaseState = $discountManager->recalculate();
if ($this->actualStep == 'cart') {
$discountManager->addUserMessages();
}
if ($data = $purchaseState->getCustomData()) {
$this->setData('discounts', $data);
}
if ($handlers = $discountManager->getActionHandlers()) {
$purchaseState->setDiscountHandlers($handlers);
}
}
}
$eventDispatcher = ServiceContainer::getService('event_dispatcher');
$purchaseState = $eventDispatcher
->dispatch(new PurchaseStateCreatedEvent($purchaseState, $this))
->getPurchaseState();
$this->purchaseUtil->ensureTotalIsNotNegative($purchaseState);
$this->purchaseUtil->persistPurchaseState($purchaseState);
}
return $this->purchaseState = $purchaseState;
}
public function getPreviousPurchaseState(): ?PurchaseState
{
return $this->previousPurchaseState;
}
public function invalidatePurchaseState(): void
{
$this->session->set('cart.purchase_state.invalid', true);
$this->purchaseState = null;
}
public function userLoggedIn($id_user)
{
$this->load();
$this->addressesSet = false;
// ################################
// PRENESENI OFFLINE KOSIKU DO LOGGED IN STAVU
$userKey = $this->getCartID();
$cartMerge = ServiceContainer::getService(CartMerge::class);
// zjistim, zda je kosik prihlaseneho uzivatele prazdnej nebo ne
$isLoggedUserCartEmpty = $cartMerge->isUserCartEmpty();
// zmerguju kosik neprihlaseneho uzivatele do kosiku prihlaseneho uzivatele
$mergedCount = $cartMerge->merge($userKey);
sqlQueryBuilder()->delete('cart')->where(Operator::equals(['user_key' => $userKey]))->execute();
// pokud kosik prihlaseneho uzivatele nebyl prazdnej a neco jsem do nej namergoval, tak pridam hlasku
if (!$isLoggedUserCartEmpty && $mergedCount > 0) {
addUserMessage(translate('cartMergeInfo', 'order'), 'info');
}
$user = User::createFromId($id_user);
if ($user && $user->user_key) {
self::setCartID($user->user_key);
}
$this->invalidatePurchaseState();
}
public function userLoggedOut()
{
$this->session->remove('cart');
$this->requestStack->getMainRequest()?->attributes->set('gtm_logout', true);
if (findModule(Modules::JS_SHOP)) {
$this->session->set(\KupShop\GraphQLBundle\EventListener\JsShopRefreshListener::SESSION_NAME, true);
}
$this->invalidatePurchaseState();
}
public function getSelectParams($alias = '')
{
$userContext = Contexts::get(UserContext::class);
if ($userContext->getActiveId() > 0) {
return [$alias.'id_user' => $userContext->getActiveId()];
} else {
return [$alias.'user_key' => $this->getCartID()];
}
}
public static function getCartID($allowEmpty = true)
{
if (!self::$cartID) {
$cookie = getVal('cartID', $_COOKIE, '');
if ($cookie) {
$userKey = $cookie;
} elseif ($allowEmpty) {
return self::$NO_CART_ID;
} else {
$userKey = session_id();
SetCookies('cartID', $userKey, 86400 * 365);
}
self::$cartID = $userKey;
}
return self::$cartID;
}
public static function setCartID($userKey = '')
{
self::$cartID = $userKey;
SetCookies('cartID', $userKey, 86400 * 365);
return self::$cartID;
}
public function clear()
{
$this->session->remove('cart');
$this->session->remove('cartData');
$this->invalidatePurchaseState();
if (findModule(\Modules::JS_SHOP)) {
$this->session->set(\KupShop\GraphQLBundle\EventListener\JsShopRefreshListener::SESSION_NAME, true);
}
return $this->deleteSQL('cart', $this->getSelectParams());
}
public function deleteItem($id)
{
$this->invalidatePurchaseState();
$this->sendCartEvent($this, CartItemEvent::BEFORE_DELETE_ITEM, new CartItemEvent($this, ['id' => $id]));
return $this->deleteSQL('cart', array_merge(['id' => $id], $this->getSelectParams()));
}
public function updateItem($id, $data)
{
if (isset($data['pieces']) && ($data['pieces'] <= 0 || !is_numeric($data['pieces']))) {
return $this->deleteItem($id);
}
$cartItemArray = sqlFetchAssoc($this->selectSQL('cart', ['id' => $id], ['id_product', 'id_variation', 'pieces', 'note']));
$product = null;
$differences = [];
if ($cartItemArray) {
$product = \Variation::createProductOrVariation($cartItemArray['id_product'], $cartItemArray['id_variation'] ?? null);
$product->createFromDB();
}
if (isset($data['note_json'])) {
$noteJson = json_decode($data['note_json'] ?: '', true) ?: [];
$data['note'] = array_replace_recursive($data['note'] ?? [], $noteJson);
unset($data['note_json']);
}
if (isset($data['note']) && $product) {
$note = $product->parseNote($cartItemArray['note']);
$note = array_replace_recursive($note, $data['note']);
$data['note'] = $product->dumpNote($note);
}
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
if (!empty($cartItemArray) && $product) {
$data['pieces'] = $this->roundPieces($product, $data['pieces']);
}
}
$affected = $this->updateSQL('cart', $data, array_merge(['id' => $id], $this->getSelectParams()));
if ($cartItemArray) {
$cartItemArrayUpdated = array_merge($cartItemArray, $data);
$differences = array_diff($cartItemArrayUpdated, $cartItemArray);
if ($differences['pieces'] ?? false) {
$differences['pieces_diff'] = $cartItemArrayUpdated['pieces'] - $cartItemArray['pieces'];
}
}
$this->sendCartEvent($this, CartItemEvent::AFTER_UPDATE_ITEM, new CartItemEvent($this, ['id' => $id] + $data, $product, $differences));
return $affected;
}
public function recalculate($items)
{
foreach ($items as $id => $data) {
if (!is_array($data)) {
$data = ['pieces' => $data];
}
$this->updateItem($id, $data);
}
$this->invalidatePurchaseState();
return true;
}
public function addItem(&$params = [])
{
/*
* Tmp fix - kvuli asynchronimu odesilani SS GTM event.
* Ve chvili kdy se dokonci celej tenhle request, tak se po zaslani response uzivateli, jeste spusti asynchronni volani odeslani do GTM.
* Jenze uvnitr se vytahuje ID sekce, coz vede k tomu ze se uvnitr MenuSectionTree vola jestli je uzivatel admin nebo ne -> vola se session,
* ale tu uz to neotevre protoze uz se odeslala response uzivateli a padne to. Divnost symfony.
* Tim, ze se to zavola tady, se to ulozi do staticky promenny a priste se uz nesaha do session.
*/
getAdminUser();
$ret = null;
if (empty($params['id_product'])) {
return $ret;
}
// get cart with allowEmpty: false parameter, to ensure the cart session and cookie is created
self::getCartID(false);
$product = Variation::createProductOrVariation($params['id_product'], $params['id_variation'] ?? null);
if (!$product->createFromDB()) {
throw new InvalidCartItemException(sprintf('Product ID %s does not exists!', $params['id_product']));
}
// Check variation is selected if product has variations
if (empty($params['id_variation']) && $product->hasVariations()) {
return -19;
}
ServiceContainer::getService('event_dispatcher')->dispatch(new CartBeforeInsertItemEvent($product, $params['note'] ?? '', $params['pieces'] ?? 0));
sqlStartTransaction();
// Encode note
if (!empty($params['note'])) {
$params['note'] = $product->dumpNote($params['note']);
}
$searchFields = array_merge($this->getSelectParams(), $this->filterFields($params, ['id_product', 'id_variation', 'note']));
$item = sqlFetchAssoc($this->selectSQL('cart', $searchFields, ['id', 'pieces']));
$piecesYet = $this->roundPieces($product, $item['pieces'] ?? 0);
$params['pieces'] = $this->roundPieces($product, $params['pieces']);
if ($piecesYet > 0) {
$this->updateItem($item['id'], ['pieces' => $item['pieces'] + $params['pieces']]);
$params['pieces'] = $item['pieces'] + $params['pieces'];
$ret = $item['id'];
} elseif ($params['pieces'] > 0) {
$params['date'] = date('Y-m-d H:i:s');
$insertFields = array_merge($this->getSelectParams(), $this->filterFields($params, ['id_product', 'id_variation', 'note', 'pieces', 'date']));
$this->insertSQL('cart', $insertFields);
$ret = sqlInsertId();
$item = ['id' => $ret];
$differences = ['pieces' => $params['pieces'], 'pieces_diff' => $params['pieces']];
$this->sendCartEvent($this, CartItemEvent::AFTER_INSERT_ITEM,
new CartItemEvent($this, $item + $params, $product, $differences)
);
}
sqlFinishTransaction();
if ($ret) {
$this->invalidatePurchaseState();
}
return $ret;
}
public function addCoupon($coupon)
{
$coupon = trim($coupon);
if (empty($coupon)) {
return false;
}
$coupon_already_used = in_array(
strtolower($coupon),
array_map(
'strtolower',
$this->coupons
));
if ($coupon_already_used) {
return true;
}
$coupon = strtoupper($coupon);
$coupon_exists = false;
if (findModule(Modules::ORDER_DISCOUNT)) {
/** @var DiscountManager $discountManager */
$discountManager = ServiceContainer::getService(DiscountManager::class);
$order_discounts = [];
if ($discountManager->couponNumberExists($coupon, $order_discounts)) {
$this->fetchProducts();
$purchaseState = $this->purchaseUtil->createStateFromCart($this);
$discountManager->setPurchaseState($purchaseState);
$discountManager->recalculate();
$coupon_exists = $discountManager->isCouponValid($coupon, $order_discounts);
}
}
if (!$coupon_exists) {
return $coupon_exists;
}
$this->coupons[$coupon] = $coupon;
$this->invalidatePurchaseState();
return true;
}
public function deleteCoupon($coupon = null)
{
if ($coupon !== null) {
$coupon = trim($coupon);
} else {
$coupon = reset($this->coupons);
}
if ($coupon) {
foreach ($this->coupons as $key => $cartCoupon) {
if (strcasecmp($cartCoupon, $coupon) == 0) {
unset($this->coupons[$key]);
break;
}
}
}
$this->invalidatePurchaseState();
return true;
}
/**
* Fetching functions.
*/
protected function fetchProducts()
{
if (!is_null($this->only_virtual_products)) {
return $this->products;
}
$dbcfg = \Settings::getDefault();
$this->totalPriceNoVat = DecimalConstants::zero();
$this->totalPriceWithVat = DecimalConstants::zero();
$this->virtual_products = false;
$this->only_virtual_products = true;
$where = queryCreate($this->getSelectParams(), true);
$qb = sqlQueryBuilder();
// TODO prepsat na ProductList a nebude toto potreba
$inStoreField = \Query\Product::getInStoreField(true, $qb);
$qb->addSelect('c.id, c.pieces, c.id_variation, c.note, p.id as id_product, p.title, p.price, p.producer as id_producer, pr.name as producer, p.discount,
FIND_IN_SET("Z", p.campaign)>0 free_shipping, COALESCE(pv.delivery_time, p.delivery_time) as delivery_time, COALESCE(pv.ean, p.ean) as ean');
$qb->addSelect("{$inStoreField} as in_store");
// prepare sub query
$subQuery = sqlQueryBuilder()
->from('cart', 'sc')
->addSelect('SUM(pieces)')
->where('sc.id_product = c.id_product AND
(sc.id_variation = c.id_variation OR sc.id_variation IS NULL AND c.id_variation IS NULL)')
->andWhere($where)
->setMaxResults(1);
$qb->addSelect("({$subQuery}) as total_pieces");
$qb->from('cart', 'c')
->addSelect(\Query\Product::withVat())
->addSelect('c.date')
->leftJoin('c', 'products', 'p', 'c.id_product=p.id')
->leftJoin('c', 'producers', 'pr', 'pr.id=p.producer')
->leftJoin('c', 'products_variations', 'pv', 'c.id_variation=pv.id');
if ($showMax = findModule('products', 'showMax')) {
$qb->addSelect('COALESCE(pv.in_store_show_max, p.in_store_show_max,'.$showMax.') as in_store_show_max');
}
if (findModule('products_variations', 'variationCode')) {
$qb->addSelect('pv.code as variation_code');
}
if (findModule('products', 'weight')) {
$qb->addSelect('COALESCE(pv.weight, p.weight) weight');
}
$qb->andWhere($where)
->groupBy('c.id')
->orderBy('c.id')
->sendToMaster();
foreach ($qb->execute() as $row) {
$IDInCart = $row['id'];
$productListId = $row['id_product'];
if ($row['id_variation']) {
$productListId .= '/'.$row['id_variation'];
}
if (!($product = $this->productList->get($productListId))) {
if ($row['id_variation']) {
$product = new Variation($row['id_product'], $row['id_variation']);
} else {
$product = new Product($row['id_product']);
}
if ($product->createFromDB($product->id) === false) {
sqlQueryBuilder()->delete('cart')
->where(\Query\Operator::equals(['id' => $row['id']]))
->execute();
continue;
}
}
$dec_Pieces = toDecimal($row['pieces']);
$Pieces = $row['pieces'];
$note = $product->parseNote($row['note']);
$InStore = max(0, $row['in_store']);
$vat = $row['vat'];
$title = $product->title;
if (!is_null($row['id_variation'])) {
$variationTitle = $product->variationTitle ?? '';
$title .= " ({$variationTitle})";
}
$price = $this->purchaseUtil->getItemPrice($row, $product);
$priceArray = PriceWrapper::wrap($price);
$price_with_vat_rounded = $priceArray['value_with_vat'];
$price_without_vat_rounded = $priceArray['value_without_vat'];
$item_price_with_vat = $price_with_vat_rounded->mul($dec_Pieces);
$item_price_without_vat = calcPrice($item_price_with_vat, -getVat($row['vat']));
$totalPriceArray = formatPrice($item_price_without_vat, getVat($row['vat']));
// celkova cena bez DPH
$this->totalPriceNoVat = $this->totalPriceNoVat->add($price_without_vat_rounded->mul($dec_Pieces));
// celkova cena s DPH
$this->totalPriceWithVat = $this->totalPriceWithVat->add($price_with_vat_rounded->mul($dec_Pieces));
// ceny podle DPH, vlozit cenu s DPH
if (!isset($this->priceByVat[$vat])) {
$this->priceByVat[$vat] = DecimalConstants::zero();
}
$this->priceByVat[$vat] = $this->priceByVat[$vat]->add($price_without_vat_rounded->mul($dec_Pieces));
if ($row['free_shipping'] > 0) {
$this->freeShipping = true;
}
// zapocitat celkovy pocet kusu
$this->totalPieces += $row['pieces'];
// Dostupnost
$availability = 0; // Vyprodano
if (!empty($row['in_store_show_max']) > 0) {
$InStore = min(intval($row['in_store_show_max']), $InStore);
}
// Simulate variation store and delivery_time
$product->deliveryTime = $row['delivery_time'];
$product->inStore = $InStore;
if ($dbcfg->prod_subtract_from_store == 'Y') {
if (max($Pieces, $row['total_pieces']) <= $InStore) {
$availability = 1;
} // Skladem
if ($availability == 0 && findModule('products_suppliers')) {
$inSuppliers = $product->getInStoreSuppliers($row['id_variation']);
$product->in_store_suppliers = intval($inSuppliers);
if ($inSuppliers > 0) {
// Je dostatek u dodavatele
$InStore += $inSuppliers;
if ($Pieces <= $InStore) {
$availability = 2;
}
}
}
$product->prepareDeliveryText();
} else {
$product->prepareDeliveryText();
if ($product->deliveryTime == 0 || $product->deliveryTimeRaw == 0) {
$availability = 1;
}
}
$prod = [];
$prod['id'] = $product->id;
$prod['id_variation'] = $row['id_variation'];
$prod['idincart'] = $IDInCart;
$prod['title'] = $title;
$prod['note'] = $note;
$prod['pieces'] = $Pieces;
$prod['total_pieces'] = $row['total_pieces'];
$prod['date'] = $row['date'];
$prod['inStore'] = $InStore;
$prod['availability'] = $availability;
$prod['deliveryTime'] = $product->deliveryTime;
$prod['deliveryTimeText'] = $product->deliveryTimeText;
$prod['deliveryTimeRaw'] = $product->deliveryTimeRaw;
$prod['discount'] = $row['discount'];
$prod['producer'] = $row['producer'];
$prod['id_producer'] = $row['id_producer'];
$prod['ean'] = $row['ean'];
$prod['vat'] = getVat($row['vat']);
$prod['product'] = $product;
$prod['virtual'] = $product->isVirtual();
$this->virtual_products = $this->virtual_products || $prod['virtual'];
$this->only_virtual_products = $this->only_virtual_products & $prod['virtual'];
$product->fetchSets();
$prod['sets'] = $product->sets;
$prod['price'] = $priceArray;
$prod['totalPrice'] = $totalPriceArray;
if (isset($row['variation_code'])) {
$prod['variation_code'] = $row['variation_code'];
}
if (findModule('products', 'weight')) {
$prod['weight'] = $row['weight'] ?: $product['weight'];
}
$this->products[$IDInCart] = $prod;
$this->productList->set($productListId, $this->products[$IDInCart]['product']);
// ----------------------------------------------------------------
// kdyz je u zbozi příplatek, ktery nebyl jeste zapocitan v cene
if (findModule(Modules::PRODUCTS_CHARGES)) {
foreach ($product->fetchCharges($note['charges'] ?? []) as $charge) {
if ($charge['included'] == 'N' && $charge['active']) {
/** @var Price $price */
$price = $charge['price'];
$chargePieces = $dec_Pieces;
if (($charge['data']['onetime'] ?? 'N') === 'Y') {
$chargePieces = DecimalConstants::one();
}
$totalPrice = new Price($price->getPriceWithoutVat()->mul($chargePieces), $price->getCurrency(), $price->getVat());
// celkova cena bez DPH
$this->totalPriceNoVat = $this->totalPriceNoVat->add($totalPrice->getPriceWithoutVat());
// celkova cena s DPH
$this->totalPriceWithVat = $this->totalPriceWithVat->add($totalPrice->getPriceWithVat());
// ceny podle DPH, vlozit cenu s DPH
if (!isset($this->priceByVat[$charge['vat']])) {
$this->priceByVat[$charge['vat']] = toDecimal(0);
}
$this->priceByVat[$charge['vat']] = $this->priceByVat[$charge['vat']]->add($totalPrice->getPriceWithoutVat());
}
}
}
}
$this->productList->fetchProducers();
$this->productList->fetchGifts();
$this->productList->fetchMainImages(4);
$this->products = array_map(function ($prod) {
$prod['image'] = $prod['product']['image'] ?? null;
return $prod;
}, $this->products);
if (empty($this->products)) {
$this->only_virtual_products = false;
}
return $this->products;
}
public function hasOnlyVirtualProducts()
{
if (is_null($this->only_virtual_products)) {
$this->fetchProducts();
}
if ($this->only_virtual_products) {
// pokud mam slevu, ve ktery je produkt, tak se nemuzu nabizet dopravu pro virtualni produkty
$productsInDiscounts = array_filter($this->getPurchaseState()->getDiscounts(), fn ($x) => $x instanceof ProductPurchaseItem && !$x->getProduct()->isVirtual());
if (!empty($productsInDiscounts)) {
$this->only_virtual_products = false;
}
}
return $this->only_virtual_products;
}
public function hasVirtualProducts()
{
if (is_null($this->virtual_products)) {
$this->fetchProducts();
}
return $this->virtual_products;
}
public function getAvailability()
{
$availability = 1;
foreach ($this->products as $product) {
switch ($product['availability']) {
case 0:
return 0;
case 2:
$availability = 2;
break;
}
}
return $availability;
}
private function fetchDiscounts()
{
$purchaseState = $this->getPurchaseState();
// add discounts from PurchaseState
if (findModule(Modules::ORDER_DISCOUNT)) {
$totalDiscount = $purchaseState->getDiscountsTotalPrice();
$this->totalDiscountPrice = $totalDiscount->getPriceWithVat()->additiveInverse();
$this->totalDiscountPriceNoVat = $totalDiscount->getPriceWithoutVat()->additiveInverse();
}
}
private function fetchVats()
{
$dbcfg = \Settings::getDefault();
// jen pokud je platcem DPH
if ($dbcfg['shop_vat_payer'] == 'Y') {
$SQL = sqlQueryBuilder()->select('id, descr, vat')
->from('vats')
->orderBy('vat');
if (findModule(Modules::OSS_VATS)) {
$vatContext = Contexts::get(VatContext::class);
if ($vatContext->isCountryOssActive()) {
$countryContext = Contexts::get(CountryContext::class);
$SQL->andWhere(\Query\Operator::equals(['id_country', $countryContext->getActiveId()]));
} else {
$SQL->andWhere('id_country IS NULL');
}
}
foreach ($SQL->execute() as $row) {
$Vat = $row['vat'];
$id = $row['id'];
if (!isset($this->priceByVat[$id])) {
$this->priceByVat[$id] = toDecimal(0);
}
// nove promenne s cenou
$priceVatArray = formatPrice($this->priceByVat[$id], $Vat, false);
$this->vats[] = [
'descr' => $row['descr'],
'tax' => $priceVatArray,
];
}
sqlFreeResult($SQL);
}
}
public function fetchCharges()
{
$purchaseState = $this->getPurchaseState();
if ($charges = $purchaseState->getCharges()) {
// add charges from PurchaseState
$this->charges = array_column($charges, 'id_charge');
$this->charges = array_fill_keys($this->charges, ['checked' => 'Y']);
$totalCharges = $purchaseState->getChargesTotalPrice();
$this->totalChargesPrice = $totalCharges->getPriceWithVat();
$this->totalChargesPriceNoVat = $totalCharges->getPriceWithoutVat();
}
}
public function field_productList(): ProductCollection
{
if (findModule(\Modules::ORDERS, \Modules::SUB_ORDERS_FROM_PURCHASE_STATE)) {
return $this->getPurchaseState()->createProductCollection();
}
return $this->productList;
}
public function field_products(): array
{
if (findModule(\Modules::ORDERS, \Modules::SUB_ORDERS_FROM_PURCHASE_STATE)) {
static $productsCache;
if ($productsCache !== null) {
return $productsCache;
}
$productCollection = $this->getPurchaseState()->createProductCollection();
$productCollection->fetchMainImages(4);
$productPurchaseItemWrapper = ServiceContainer::getService(ProductPurchaseItemWrapper::class);
$products = [];
foreach ($this->getPurchaseState()->getProducts() as $key => $product) {
$products[$key] = (clone $productPurchaseItemWrapper)->setObject($product);
}
return $productsCache = $products;
}
return $this->products;
}
/**
* Implements ArrayAccess interface.
*/
public function offsetSet($offset, $value): void
{
$this->{$offset} = $value;
}
public function offsetExists($offset): bool
{
return isset($this->{$offset});
}
public function offsetUnset($offset): void
{
unset($this->{$offset});
}
public function offsetGet($offset): mixed
{
$method = $this->offsetMethod($offset);
if (method_exists($this, $method)) {
return call_user_func([$this, $method]);
}
return isset($this->{$offset}) ? $this->{$offset} : null;
}
public function offsetMethod($offset)
{
return 'field_'.$offset;
}
private function fetchDelivery()
{
if (!findModule('eshop_delivery')) {
return;
}
if ($this->transport > 0) {
$deliveryType = $this->getDeliveryType();
if (!$deliveryType) {
return;
}
if (empty($this->delivery_id)) {
$this->delivery_id = $deliveryType->id_delivery;
}
if (empty($this->payment_id)) {
$this->payment_id = $deliveryType->id_payment;
}
$deliveryType->getDelivery()->applyToCart($this);
if ($this->storeDeliveryData && (findModule('payments') || findModule('deliveries'))) {
$payment = $deliveryType->getPayment();
if ($payment) {
$this->paymentData = $payment->storePaymentInfo();
} else {
$this->paymentData = [];
}
$delivery = $deliveryType->getDelivery();
if ($delivery) {
$this->deliveryData = $delivery->storeDeliveryInfo(getVal($delivery->id, getVal('delivery_data')));
} else {
$this->deliveryData = [];
}
}
} else {
if ($this->delivery_id) {
$deliveries = Delivery::getAll(false);
$delivery = getVal($this->delivery_id, $deliveries);
$this->deliveryData = $delivery ? $delivery->storeDeliveryInfo(getVal($delivery->id, getVal('delivery_data'))) : [];
}
}
}
public function getTotalPriceForDelivery()
{
assert($this->initialized, 'Can not call getTotalPriceForDelivery() before Cart initialization - createFromDB');
if (findModule(Modules::ORDERS, Modules::SUB_ORDERS_FROM_PURCHASE_STATE)) {
$totalPriceWithVat = $this->getPurchaseState()->getProductsTotalPrice()->getPriceWithVat();
} else {
$totalPriceWithVat = $this->totalPriceWithVat;
}
if (findModule(Modules::DELIVERY_TYPES, Modules::SUB_FREE_DELIVERY_NO_DISCOUNT)) {
$totalPriceForDelivery = $totalPriceWithVat;
} else {
// TotalPrice darkovych poukazu
$useCouponTotalPrice = $this->purchaseUtil->getUseCouponTotalPrice($this->getPurchaseState());
// Aplikovaný poukaz nesnižuje hodnotu objednávky pro dopravu zdarma.
$totalDiscountPrice = $this->totalDiscountPrice->add($useCouponTotalPrice->getPriceWithVat());
$totalPriceForDelivery = Decimal::max($totalPriceWithVat->sub($totalDiscountPrice), DecimalConstants::zero());
}
if (!findModule(Modules::DELIVERY_TYPES, Modules::SUB_FREE_DELIVERY_NO_CHARGES)) {
$totalPriceForDelivery = Decimal::max($totalPriceForDelivery->add($this->totalChargesPrice), DecimalConstants::zero());
}
$currencyContext = ServiceContainer::getService(\KupShop\KupShopBundle\Context\CurrencyContext::class);
return new Price($totalPriceForDelivery, $currencyContext->getActive(), 0);
}
protected function fetchTotalsFromPurchaseState(PurchaseState $purchaseState)
{
$total = $purchaseState->getProductsTotalPrice();
$this->totalPriceWithVat = $total->getPriceWithVat();
$this->totalPriceNoVat = $total->getPriceWithoutVat();
$total = $purchaseState->getDiscountsTotalPrice();
$this->totalDiscountPrice = $total->getPriceWithVat()->additiveInverse();
$this->totalDiscountPriceNoVat = $total->getPriceWithoutVat()->additiveInverse();
$total = $purchaseState->getChargesTotalPrice();
$this->totalChargesPrice = $total->getPriceWithVat();
$this->totalChargesPriceNoVat = $total->getPriceWithoutVat();
$total = $purchaseState->getTotalPrice();
$this->totalPricePay = $total->getPriceWithVat();
$this->totalPricePayNoVat = $total->getPriceWithoutVat();
}
protected function fetchTotals()
{
// celkova cena k zaplaceni
$this->totalPriceWithVat = toDecimal($this->totalPriceWithVat);
$this->totalPricePay = Decimal::max($this->totalPriceWithVat->sub($this->totalDiscountPrice)->add($this->totalChargesPrice), DecimalConstants::zero());
$this->totalPriceNoVat = toDecimal($this->totalPriceNoVat);
$this->totalPricePayNoVat = Decimal::max($this->totalPriceNoVat->sub($this->totalDiscountPriceNoVat)->add($this->totalChargesPriceNoVat), DecimalConstants::zero());
$currencyContext = ServiceContainer::getService(\KupShop\KupShopBundle\Context\CurrencyContext::class);
if (findModule('eshop_delivery')) {
foreach ($this->getDeliveryTypes() as &$delivery) {
if ($delivery->id == $this->transport) {
$this->selected = true;
$delivery->attachToCart($this);
}
}
unset($delivery);
// Add delivery price
if ($this->transport && $this->getActualStepIndex() > 0) {
if (isset($this->delivery_types[$this->transport])) {
$delivery = $this->delivery_types[$this->transport];
// Kdyz uplatnujeme kupon, tak se musi tykat i dopravy. Teoreticky by mohlo jit vsude SUB_APPLY_DISCOUNT_ON_DELIVERY
// zapnout, ale nechcem nic rozbit
$isCoupon = false;
foreach ($this->getPurchaseState()->getDiscounts() as $discount) {
if (($discount->getNote()['discount_type'] ?? '') == 'use_coupon') {
$isCoupon = true;
break;
}
}
$apply_discount_on_delivery = findModule(Modules::DELIVERY_TYPES, Modules::SUB_APPLY_DISCOUNT_ON_DELIVERY) || $isCoupon;
$totalPricePay = new Price($apply_discount_on_delivery ? $this->totalPriceWithVat : $this->totalPricePay, $currencyContext->getActive(), 0);
$totalPricePayNoVat = new Price($apply_discount_on_delivery ? $this->totalPriceNoVat : $this->totalPricePayNoVat, $currencyContext->getActive(), $delivery->vat);
$this->totalPricePay = PriceCalculator::add($totalPricePay, $delivery->getPrice())->getPriceWithVat(false);
$this->totalPricePayNoVat = PriceCalculator::add($totalPricePayNoVat, $delivery->getPrice())->getPriceWithoutVat(false);
if ($apply_discount_on_delivery) {
$this->totalPricePay = Decimal::max($this->totalPricePay->sub($this->totalDiscountPrice)->add($this->totalChargesPrice), DecimalConstants::zero());
$this->totalPricePayNoVat = Decimal::max($this->totalPricePayNoVat->sub($this->totalDiscountPriceNoVat)->add($this->totalChargesPriceNoVat), DecimalConstants::zero());
}
} else {
$this->delivery_id = null;
$this->setTransport(null);
}
}
}
// Round total price
$this->totalPricePay = roundPrice($this->totalPricePay, $currencyContext->getActive()->getPriceRoundOrder(), bata: false);
}
public function getActualStepIndex()
{
$steps = array_keys($this->steps);
return array_search($this->actualStep, $steps);
}
public function save(?array $filter = null)
{
$serializeFields = ['coupons', 'invoice', 'delivery', 'transport', 'delivery_id', 'payment_id',
'note', 'addressesSet', 'paymentData', 'deliveryData', 'newsletter', 'register', 'max_step', 'max_step_submitted',
'actionHandlersData', 'userOrderNo', ];
if ($filter) {
$serializeFields = array_intersect($serializeFields, $filter);
}
$data = [];
foreach ($serializeFields as $field) {
$data[$field] = $this->$field;
}
$this->session->set('cart', serialize($data));
}
public function load(?array $filter = null)
{
$data = $this->session->get('cart');
if (!$data) {
return false;
}
$data = unserialize($data);
foreach ($data as $key => $value) {
if (!$filter || in_array($key, $filter)) {
$this->$key = $value;
}
}
return true;
}
public function setData($key, $value)
{
$data = $this->getData();
if (!$key) {
$data = array_merge($data, $value);
} else {
$data[$key] = $value;
}
$this->session->set('cartData', $data);
}
/**
* @return bool|mixed
*/
public function getData($key = null)
{
$data = $this->session->get('cartData', []);
if ($key) {
if (!empty($data[$key])) {
return $data[$key];
} else {
return false;
}
}
return $data;
}
public function fetchAddresses()
{
if (!$this->addressesSet) {
$this->user->id = Contexts::get(UserContext::class)->getActiveId();
if ($this->user->fetchAddresses()) {
return;
}
// Select first enabled country
if ($this->getDeliveryType()) {
$countries = $this->getDeliveryType()->getDelivery()->getSupportedCountries();
$this->delivery['country'] = $this->invoice['country'] = reset($countries);
}
}
}
public function getTotalWeight()
{
$weight = 0.;
foreach ($this->products as $product) {
if ($product['pieces'] > 0) {
$weight += ($product['weight'] ?? 0) * $product['pieces'];
}
}
return $weight;
}
public function getMaxItemWeight()
{
$maxItemWeight = 0.;
foreach ($this->products as $product) {
if (!empty($product['sets'])) {
foreach ($product['sets'] as $set_product) {
if ($set_product['weight'] > $maxItemWeight) {
$maxItemWeight = $set_product['weight'];
}
}
} else {
if ($product['weight'] > $maxItemWeight) {
$maxItemWeight = $product['weight'];
}
}
}
return $maxItemWeight;
}
public function updateAddresses($invoice, $delivery)
{
$this->addressesSet = true;
$previousDic = $this->invoice['dic'] ?? null;
$requires_delivery = true;
if ($this->delivery_id) {
// we need to update address before getDeliveries call
$this->user->updateAddresses($invoice, $delivery);
$deliveries = Delivery::getAll(false);
if (isset($deliveries[$this->delivery_id])) {
$requires_delivery = $deliveries[$this->delivery_id]->requiresDeliveryAddress();
}
}
$error = $this->user->updateAddresses($invoice, $delivery, !$requires_delivery);
$emailCheck = ServiceContainer::getService(EmailCheck::class);
if (!$emailCheck->isEmailDomainValid($invoice['email'])) {
throw new Exception(sprintf(translate('error', 'user')['invalid_email_domain'], htmlentities($invoice['email'])));
}
// detect dic change
$reloadCartProducts = false;
if ($previousDic !== ($invoice['dic'] ?? null)) {
// save cart to session so UserContext::isVatPayer can load info about set DIC
$this->save();
// reload cart products to load products with correct vat
// PurchaseState is then created from cart products so we need products with correct vats loaded
$reloadCartProducts = true;
}
// Remember selected country in CountryContext - keeps CountryContext and cart delivery_country in sync
$deliveryCountry = ($this->delivery['country'] ?? false) ?: $this->invoice['country'] ?? false;
if ($deliveryCountry) {
/** @var CountryContext $countryContext */
$countryContext = Contexts::get(CountryContext::class);
if ($deliveryCountry != $countryContext->getActiveId()) {
$countryContext->activate($deliveryCountry);
$countryContext->remember($deliveryCountry);
// Force to recalculate product prices
// HACK: Asi by to chtělo nějakou metodu na to, ne? Tohleje hnus.
$reloadCartProducts = true;
}
}
if ($reloadCartProducts) {
$vatContext = Contexts::get(VatContext::class);
$vatContext->clearCache();
$this->only_virtual_products = null;
$this->fetchProducts();
}
return $error;
}
public function registerUser($password)
{
$this->register = false;
if ($password) {
$error = $this->user->sanitizeRegistration($password);
if (!$error) {
$this->register = $password;
}
} else {
$error = null;
}
return $error;
}
public function setTransport($transport_id)
{
$this->transport = $transport_id;
$this->storeDeliveryData = true;
$this->invalidatePurchaseState();
return true;
}
public function setDeliveryAndPayment($delivery_id, $payment_id)
{
if ($delivery_id < 0) {
$this->resetDeliveryType();
}
if ($payment_id < 0) {
$this->resetDeliveryType(false);
}
if ($delivery_id > 0) {
$this->delivery_id = $delivery_id;
}
if ($payment_id) {
$this->payment_id = $payment_id;
}
$payment_id = explode('-', $this->payment_id)[0];
if ($this->hasOnlyVirtualProducts()) {
foreach ($this->getVirtualDeliveryTypes() as $delivery_type) {
if ($delivery_type->id_delivery == $this->delivery_id && $delivery_type->id_payment == $payment_id) {
return $this->setTransport($delivery_type->id);
}
}
}
foreach (DeliveryType::getAll(false) as $delivery_type) {
if ($delivery_type->id_delivery == $this->delivery_id && $delivery_type->id_payment == $payment_id) {
return $this->setTransport($delivery_type->id);
}
}
$this->resetDeliveryType(false);
return false;
}
public function resetDeliveryType(bool $resetDelivery = true): void
{
if ($resetDelivery) {
$this->delivery_id = null;
}
$this->transport = null;
$this->payment_id = null;
$this->invalidatePurchaseState();
}
/**
* @return DeliveryType[]
*/
public function getVirtualDeliveryTypes()
{
$delivery_types = DeliveryType::getAll(true);
if ($this->initialized) {
$totalPrice = $this->getTotalPriceForDelivery();
} else {
$totalPrice = new Price($this->totalPriceWithVat, Contexts::get(CurrencyContext::class)->getActive(), 0);
}
$delivery_types = array_filter($delivery_types, function ($d) use ($totalPrice) {
return $d->delivery_class == 'VirtualDelivery' && $d->getPayment()->accept($totalPrice, $this->freeShipping);
});
return $delivery_types;
}
public function getDeliveryTypes(): array
{
if (empty($this->delivery_types)) {
if ($this->hasOnlyVirtualProducts()) {
$this->delivery_types = $this->getVirtualDeliveryTypes();
}
if (empty($this->delivery_types)) {
if ($this->getPurchaseState()->isFreeDelivery()) {
$this->freeShipping = true;
}
$totalPrice = $this->getTotalPriceForDelivery();
$this->delivery_types = Order::getDeliveryTypeList($totalPrice, $this->freeShipping, null, null, $this->getPurchaseState());
}
}
return $this->delivery_types;
}
/**
* @return Delivery[]
*/
public function getDeliveries()
{
$deliveries = [];
foreach ($this->getDeliveryTypes() as $delivery_type) {
$delivery = $delivery_type->getDelivery();
$deliveries[$delivery->id] = $delivery;
}
$totalPriceForDelivery = $this->getTotalPriceForDelivery();
foreach ($deliveries as $delivery) {
if ($this->delivery_id == $delivery['id']) {
$delivery['selected'] = true;
// Load payment class info
if (findModule('deliveries') && !empty($delivery['class'])) {
$delivery->loadDeliveryInfo($this->deliveryData);
}
}
if (!$delivery->accept($totalPriceForDelivery, $this->freeShipping, $this->getPurchaseState())) {
unset($deliveries[$delivery['id']]);
}
$delivery->applyToCart($this);
try {
$delivery->check($this);
} catch (\KupShop\OrderingBundle\Exception\DeliveryException $e) {
// ignorujeme, check volany kvuli pripadnemu disablovani dopravy pomoci exception
$delivery->setException($e);
}
}
return $deliveries;
}
public function getLowestDeliveryPrice()
{
$minPrice = Decimal::create(0);
foreach ($this->getDeliveries() as $delivery) {
/** @var Decimal $price */
$price = $delivery['price']['value_with_vat'];
if ($minPrice->asInteger() == 0 && $price > $minPrice) {
$minPrice = $price;
} else {
if ($minPrice > $price && $price->asInteger() != 0) {
$minPrice = $price;
}
}
}
return $minPrice;
}
public function getPayments()
{
$all_payments = DeliveryType::getPayments($this->hasOnlyVirtualProducts());
if ($this->hasOnlyVirtualProducts()) {
$delivery_types = $this->getVirtualDeliveryTypes();
$payments = [];
foreach ($delivery_types as $delivery_type) {
if (array_key_exists($delivery_type->id_payment, $all_payments)) {
$payments[$delivery_type->id_payment] = $all_payments[$delivery_type->id_payment];
}
}
}
if (empty($payments)) {
$payments = $all_payments;
}
$currencyContext = ServiceContainer::getService(\KupShop\KupShopBundle\Context\CurrencyContext::class);
$paymentsHasDelivery = $this->getPaymentHasDelivery();
foreach ($payments as &$payment) {
if (!array_key_exists($payment['id'], $paymentsHasDelivery)) {
unset($payments[$payment['id']]);
continue;
}
$payment['disabled'] = false;
if ($this->payment_id == $payment['id']) {
$payment['selected'] = true;
}
if ($this->paymentDisabled($payment['id'])) {
$payment['disabled'] = true;
}
$totalPrice = new Price(toDecimal($this->totalPricePay), $currencyContext->getActive(), 0);
if ($payment['price_dont_countin_from'] && PriceCalculator::firstLower($payment['price_dont_countin_from'], $totalPrice)) {
$payment['price'] = formatCustomerPrice(0);
}
if ($payment['class']) {
try {
$payment['class']->check($this);
} catch (\KupShop\OrderingBundle\Exception\PaymentException $e) {
// ignorujeme, check volany kvuli pripadnemu disablovani platby pomoci exception
$payment['class']->setException($e);
$payment['exception'] = $payment['class']->exception;
}
}
}
return $payments;
}
protected function getPaymentHasDelivery()
{
$paymentsHasDelivery = [];
foreach ($this->delivery_types as $type) {
$paymentsHasDelivery[$type->id_payment] = true;
}
return $paymentsHasDelivery;
}
public function paymentDisabled($paymentId)
{
$disabled = true;
foreach ($this->delivery_types as $type) {
if ($this->delivery_id == $type->id_delivery && $paymentId == $type->id_payment) {
$disabled = false;
}
}
return $disabled;
}
/**
* @return DeliveryType
*/
public function getDeliveryType()
{
return getVal($this->transport, $this->delivery_types);
}
public function setNote($note)
{
$this->note = $note;
}
public function setUserOrderNo($userOrderNo)
{
$this->userOrderNo = $userOrderNo;
}
// Order Submission
public function submitOrder($languageID = null)
{
QueryHint::routeToMaster();
$error = $this->checkCart();
$emailCheck = ServiceContainer::getService(EmailCheck::class);
if (!$emailCheck->isEmailDomainValid($this->invoice['email'] ?? '')) {
$error = 10;
}
if (empty($error)) {
// Get delivery
if ($this->hasOnlyVirtualProducts()) {
$this->deliveryType = ($this->getVirtualDeliveryTypes()[$this->transport] ?? DeliveryType::get($this->transport));
} else {
$this->deliveryType = DeliveryType::get($this->transport);
}
// pokud je vyplnena dodaci adresa,
// tak nebudeme mergovat, jinak muze vzniknout chyba v dodaci adrese
$filter_delivery = array_filter($this->delivery, function ($value, $key) { return !empty($value) && ($key != 'country') && ($key != 'currency'); }, ARRAY_FILTER_USE_BOTH);
if (!$filter_delivery) {
$this->delivery['country'] = '';
$this->delivery = array_merge($this->invoice, array_filter($this->delivery));
}
// Activate country context from delivery country
if ($this->delivery['country'] ?? false) {
Contexts::get(CountryContext::class)->activate($this->delivery['country']);
}
// Nejhorsi - fallback na doplneni v pripade, ze z kosiku prijde prazdno
if (empty($this->invoice['country'])) {
$sentry = getRaven();
$sentry->captureMessage('Order create: missing invoice_country!', [], ['cart' => $this, 'invoice' => $this->invoice, 'delivery' => $this->delivery]);
$this->invoice['country'] = Contexts::get(CountryContext::class)->getActiveId();
}
if (sqlGetConnection()->getTransactionNestingLevel() <= 0) {
sqlQuery('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
}
$order = sqlGetConnection()->transactional(fn () => $this->submitOrderTransactional($languageID));
// check if order exists in db
$id_order = sqlQueryBuilder()->select('id')->from('orders')
->where(Operator::equals(['id' => $order->id]))
->execute()->fetchOne();
if (!$id_order) {
$sentry = getRaven();
$sentry->captureMessage("submitOrder error - returned order that doesn't exist!", [], ['order' => $order, 'cart' => $this]);
throw new \RuntimeException(translate('order_failed_try_again', 'order_error'));
}
$this->sendErrorIgnoringOrderEvent($order, OrderEvent::ORDER_FINISHED);
return $order;
}
return $error;
}
public function submitOrderTransactional($languageID): Order
{
global $cfg;
// cartParams => ukladame pred vytvorenim uzivatele behem objednavkoveho procesu - aby se nam neztratil kosik
$cartParams = $this->getSelectParams();
// Synchronization point. Lock all current cart items to avoid multiple cart submission
$SQL = sqlQueryBuilder()->select('c.*')
->forUpdate()
->from('cart', 'c')
->andWhere(queryCreate($cartParams, true))
->execute();
// During locking, all cart items disappeared - probably already ordered and present in another order
if ($SQL->rowCount() <= 0) {
// TODO: tmp log kvuli zdvojenym objednavkam
$this->kibanaLog(
'Cart::submitOrder - duplicate order submit',
[
'cartID' => self::getCartID(),
]
);
throw new CartValidationException(translate('duplicate_order', 'order_error'));
}
$id_user = $this->submitOrderRegister();
if (!isset($languageID)) {
$languageID = ServiceContainer::getService(
\KupShop\KupShopBundle\Context\LanguageContext::class
)->getActiveId();
}
$fields = [
'id_user' => $id_user,
'order_no' => '_'.rand(0, 999999),
'status' => 0,
'invoice_name' => $this->invoice['name'] ?? '',
'invoice_surname' => $this->invoice['surname'] ?? '',
'invoice_firm' => $this->invoice['firm'] ?? '',
'invoice_ico' => $this->invoice['ico'] ?? '',
'invoice_dic' => $this->invoice['dic'] ?? '',
'invoice_street' => $this->invoice['street'] ?? '',
'invoice_city' => $this->invoice['city'] ?? '',
'invoice_zip' => $this->invoice['zip'] ?? '',
'invoice_country' => $this->invoice['country'] ?? '',
'invoice_phone' => $this->invoice['phone'] ?? '',
'invoice_email' => $this->invoice['email'] ?? '',
'invoice_copy_email' => $this->invoice['copy_email'] ?? '',
'invoice_state' => $this->invoice['state'] ?? '',
'invoice_custom_address' => $this->invoice['custom_address'] ?? '',
'delivery_name' => $this->delivery['name'] ?? '',
'delivery_surname' => $this->delivery['surname'] ?? '',
'delivery_firm' => $this->delivery['firm'] ?? '',
'delivery_street' => $this->delivery['street'] ?? '',
'delivery_city' => $this->delivery['city'] ?? '',
'delivery_zip' => $this->delivery['zip'] ?? '',
'delivery_country' => $this->delivery['country'] ?? '',
'delivery_state' => $this->delivery['state'] ?? '',
'delivery_custom_address' => $this->delivery['custom_address'] ?? '',
'delivery_phone' => $this->delivery['phone'] ?? '',
'delivery_email' => $this->delivery['delivery_email'] ?? '',
'delivery_type' => $this->deliveryType['name'],
'id_delivery' => $this->deliveryType['id'] > 0 ? $this->deliveryType['id'] : null,
'delivery_complete' => 0,
'note_user' => $this->note,
'user_order_no' => $this->userOrderNo,
'date_created' => date('Y-m-d H:i:s'),
'date_updated' => date('Y-m-d H:i:s'),
];
if (findModule(Modules::CURRENCIES)) {
$currency = Contexts::get(CurrencyContext::class)->getActive();
$fields['id_language'] = $languageID;
$fields['currency'] = $currency->getId();
$fields['currency_rate'] = $currency->getRate()->asString();
}
if (!empty($this->note) && !empty($cfg['Order']['Flags']) && !empty($cfg['Order']['Flags']['R'])) {
$fields['flags'] = getVal('flags', $fields).',R';
}
if (findModule(Modules::ORDERS, Modules::SUB_CREATE_ORDER_FROM_PURCHASE_STATE)) {
$this->purchaseState->setCustomData([
'order' => $fields,
'delivery_data' => $this->deliveryData,
'payment_data' => $this->paymentData,
]);
$order = $this->purchaseUtil->createOrderFromPurchaseState($this->purchaseState);
$this->submitOrderData($order);
$this->submitOrderDeliveries($order);
$this->submitOrderPayments($order);
$this->clear();
return $order;
}
$this->insertSQL('orders', $fields);
// ID nove ulozene objednavky
$IDorder = sqlInsertId();
// Create Order object
$order = new Order();
$order->createFromDB($IDorder);
// set PurchaseState from cart to order
$order->setPurchaseState($this->getPurchaseState());
// Set status indicating order creation
$order->status = -1;
$this->sendOrderEvent($order, OrderEvent::ORDER_CREATED);
// ulozit vybrane zbozi do objednavky
$where = queryCreate($cartParams, true);
$SQL = sqlQueryBuilder()->select("c.id as cart_id, c.pieces, c.id_variation, c.note, p.id as product_id, p.title, p.code, p.price, p.discount,
FIND_IN_SET('Z', p.campaign)>0 free_delivery")
->from('cart', 'c')
->addSelect(\Query\Product::withVat())
->leftJoin('c', 'products', 'p', 'c.id_product=p.id')
->andWhere($where)
->groupBy('c.id')
->orderBy('c.id'); // order asc by id, so the items will be added in the same order that they were added to the cart
// Traverse all ordered products
if (findModule(\Modules::ORDERS, \Modules::SUB_ORDERS_FROM_PURCHASE_STATE)) {
$this->insertItemsFromPurchaseState($order);
} else {
$purchaseStateProducts = $order->getPurchaseState()->getProducts();
foreach ($SQL->execute() as $row) {
$IDInCart = $row['cart_id'];
$IDprod = $row['product_id'];
$IDvariation = $row['id_variation'];
$Pieces = $row['pieces'];
$Note = $row['note'];
$productPurchaseItem = null;
if (array_key_exists($IDInCart, $purchaseStateProducts)) {
$productPurchaseItem = $purchaseStateProducts[$IDInCart];
$Pieces = $productPurchaseItem->getPieces();
// Pieces nemusi sedet v pripade slevy na nejlevnejsi produkt - kdyz toho nejlevnejsiho produktu mam napr. 5 kusu,
// tak v purchaseState budou rozdeleny na 2 ProductPurchaseItem - 1 ks se slevou + 4 ks bez slevy
// v tabulce cart 5 ks toho produktu jsou dohromady ($Pieces = 5), ale $productPurchaseItem->getPieces() = 4
// 1 ks se slevou bude pridan pozdeji (v recalculateFull)
}
$res = $order->insertItem($IDprod, $IDvariation, $Pieces, $Note);
// vymazat zbozi z kosiku
$this->deleteSQL('cart', ['id' => $IDInCart]);
if ($productPurchaseItem) {
$productPurchaseItem->setId($res);
}
}
}
$order->recalculateFull($this->deliveryType['id'], null, true, true);
try {
$this->sendErrorIgnoringOrderEvent($order, OrderEvent::ORDER_COMPLETE);
$this->submitOrderData($order);
$this->submitOrderDeliveries($order);
$this->submitOrderPayments($order);
$this->clear();
} catch (Throwable $e) {
if (isDevelopment()) {
throw $e;
}
// send to sentry and continue
$logger = getRaven();
$logger->captureException($e);
}
return $order;
}
protected function insertItemsFromPurchaseState(Order $order): void
{
foreach ($order->getPurchaseState()->getProducts() as $item) {
$IdInCart = $item->getId();
$order->insertProductPurchaseItem($item);
// vymazat zbozi z kosiku
$this->deleteSQL('cart', ['id' => $IdInCart]);
}
}
protected function checkCart()
{
$this->sendCartEvent($this, CartEvent::CHECK_CART);
// Check delivery type selected
if (findModule('eshop_delivery')) {
$deliveryType = $this->getDeliveryType();
if (!$deliveryType) {
return 5;
}
$error = $deliveryType->check($this);
if ($error) {
return $error;
}
}
// Check cart is not empty
$products = sqlQueryBuilder()->select('COUNT(c.id)')
->from('cart', 'c')
->join('c', 'products', 'p', 'c.id_product=p.id')
->where(\Query\Operator::equals($this->getSelectParams()))
->andWhere(Translation::joinTranslatedFields(ProductsTranslation::class,
function ($qb, $columnName, $translatedField) {
$qb->andWhere(\Query\Operator::coalesce($translatedField, 'p.figure').' = "Y" ');
}, ['figure']))
->sendToMaster()
->execute()->fetchColumn();
if (intval($products) == 0) {
return 12;
}
$dbcfg = Settings::getDefault();
$nvProducts = [];
// check if products in cart can be purchased
if (($dbcfg->prod_sell_with_zero_price ?? 'N') != 'Y') {
foreach ($this->fetchProducts() as $item) {
/** @var Product $product */
$product = $item['product'];
// Pokud se jedná o multiset tak master produkt je za 0kč (a je viditelný) -> skip
if (array_key_exists('MS', $product->campaign_codes) && $product->visible == 'Y') {
continue;
}
// beru cenu od produktu, protoze kdybych bral cenu od polozky v kosiku, tak to muze zpusobit akorat
// problemy na shopech, kde maji nejaky customizace a chteji prodavat za 0 Kc (viz. sartor a vzorky latek)
if (!$product->getProductPrice()->getPriceWithVat()->isPositive() || $product->visible != 'Y') {
$nvProducts[$item['id']] = $item['id_variation'];
}
}
}
if (count($nvProducts) > 0) {
$this->errors[self::$ERR_CANNOT_BE_PURCHASED] = $nvProducts;
$ids = implode(',', $nvProducts);
logError(__FILE__, __LINE__, "Produkty {$ids} nelze zakoupit.");
return 33;
}
// Check all items are in store if required
return $this->checkCartItems();
}
public function checkCartItems(&$errors = null)
{
$event = $this->sendCartEvent($this, CartEvent::CHECK_CART_ITEMS);
$where = queryCreate($this->getSelectParams(), true);
$products = sqlQueryBuilder()->select('p.id as id_product')
->from('cart', 'c')
->leftJoin('c', 'products', 'p', 'c.id_product=p.id')
->leftJoin('p', 'products_variations', 'pv2', 'pv2.id_product=p.id')
->leftJoin('c', 'products_variations', 'pv', 'pv.id=c.id_variation')
->where(Operator::equals($this->getSelectParams()))
->andWhere(Translation::joinTranslatedFields(VariationsTranslation::class,
function ($qb, $columnName, $translatedField) {
$qb->andWhere(
Operator::orX(
Operator::coalesce($translatedField, 'pv.figure').' = "N" ',
'c.id_variation IS NULL AND pv2.id IS NOT NULL'));
}, ['figure']))
->groupBy('c.id')
->sendToMaster()
->execute();
if (!empty($products->rowCount())) {
logError(__FILE__, __LINE__, 'WARN: Není vybrana varianta');
$this->errors[self::$ERR_NOT_SELECTED_VARIATION] = $errors = [$products->fetchOne()];
return 31;
}
// Check for products not on stock
if ($event->getErrors()[\Cart::$ERR_OUT_OF_STOCK] ?? false) {
$this->errors[self::$ERR_OUT_OF_STOCK] = $errors = $event->getErrors()[\Cart::$ERR_OUT_OF_STOCK];
return 30;
}
if (findModule('eshop_delivery')) {
if ($this->max_step != 0) {
$deliveryType = $this->getDeliveryType();
if ($deliveryType) {
$error = $deliveryType->check($this);
if ($error) {
return $error;
}
}
}
}
return null;
}
public function ensureCartInStore()
{
$dbcfg = \Settings::getDefault();
if ($dbcfg->order_availability === \Settings::ORDER_AVAILABILITY_ALL) {
return null;
}
// Check for products not on stock
$count = $this->getEnsureInStoreQueryBuilder()->execute();
sqlQueryBuilder()
->delete('cart')
->where(\Query\Operator::equals($this->getSelectParams()))
->andWhere('pieces <= 0')
->execute();
$this->invalidatePurchaseState();
if (findModule(\Modules::JS_SHOP)) {
$this->session->set(\KupShop\GraphQLBundle\EventListener\JsShopRefreshListener::SESSION_NAME, true);
}
return $count;
}
protected function getEnsureInStoreQueryBuilder(): QueryBuilder
{
$qb = sqlQueryBuilder()
->update('cart', 'c')
->leftJoin('c', 'products', 'p', 'c.id_product=p.id')
->leftJoin('c', 'products_variations', 'pv', 'c.id_variation=pv.id')
->andWhere(\Query\Operator::equals($this->getSelectParams()));
$new_pieces = $this->getEnsureInStoreNewPiecesField($qb);
return $qb->set('c.pieces', $new_pieces)
->andWhere('p.id IS NOT NULL AND '.$new_pieces.' < c.pieces');
}
protected function getEnsureInStoreNewPiecesField(QueryBuilder $qb): string
{
$new_pieces = \Query\Product::getInStoreField(true, $qb);
if (findModule(\Modules::PRODUCTS_SUPPLIERS)) {
// in stock = in_store (pokud je > 0) + skladem u dodavatele
$new_pieces = "GREATEST(0, {$new_pieces})";
$new_pieces .= ' + (SELECT COALESCE(SUM(pos.in_store), 0) FROM products_of_suppliers pos WHERE p.id = pos.id_product AND ((pv.id IS NULL and pos.id_variation IS NULL) OR (pv.id=pos.id_variation)))';
}
$new_pieces = "GREATEST(0, {$new_pieces})";
$showMax = findModule('products', 'showMax', 0);
if ($showMax > 0) {
$new_pieces = "LEAST({$new_pieces}, COALESCE(pv.in_store_show_max, p.in_store_show_max, {$showMax}))";
}
return $new_pieces;
}
protected function getCheckNotInStoreQueryBuilder(): QueryBuilder
{
$qb = sqlQueryBuilder();
$qb->select('p.id, pv.id as id_variation', \Query\Product::getProductInStock($qb).' as in_store')
->from('cart', 'c')
->leftJoin('c', 'products', 'p', 'c.id_product = p.id')
->leftJoin('c', 'products_variations', 'pv', 'c.id_variation=pv.id')
->andWhere('p.id IS NOT NULL')
->andWhere(\Query\Operator::equals($this->getSelectParams()))
->having('in_store < SUM(c.pieces)')
->groupBy('p.id, pv.id');
if (findModule(Modules::PRODUCTS, 'order_not_in_store')) {
$qb->andWhere(\Query\Operator::not('FIND_IN_SET("PR", p.campaign)'));
}
return $qb;
}
/**
* @param $order Order
*/
protected function submitOrderData($order)
{
foreach ($this->getData() as $key => $value) {
$order->setData($key, $value);
}
$userContent = ServiceContainer::getService(\KupShop\OrderingBundle\Util\UserContent\UserContent::class);
if ($value = $userContent->getData('cart')) {
$order->setData('user-content', $value);
}
}
/**
* @param $order Order
*/
protected function submitOrderDeliveries($order)
{
// -------------------------------------------------------------------
// ZJISTENI ZDA JE E-PLATBA V ESHOPU A K TOMUTO DRUHU DORUCENI
// -------------------------------------------------------------------
if (findModule('deliveries')) {
if (!empty($this->deliveryData)) {
$order->setData('delivery_data', $this->deliveryData);
}
}
}
/**
* @param $order Order
*/
protected function submitOrderPayments($order)
{
// -------------------------------------------------------------------
// ZJISTENI ZDA JE E-PLATBA V ESHOPU A K TOMUTO DRUHU DORUCENI
// -------------------------------------------------------------------
if (findModule('payments')) {
$order->setData('payment_data', $this->paymentData);
}
}
protected function sendOrderEvent($order, $eventName)
{
$event = new OrderEvent($order);
$event->setCart($this);
$this->getEventDispatcher()->dispatch($event, $eventName);
}
protected function sendErrorIgnoringOrderEvent($order, $eventName)
{
$event = new OrderEvent($order);
$event->setCart($this);
$this->getErrorIgnoringEventDispatcher()->dispatch($event, $eventName);
}
protected function sendCartEvent($cart, $eventName, $event = null)
{
$event = $event ?: new CartEvent($cart);
$this->getEventDispatcher()->dispatch($event, $eventName);
return $event;
}
private $eventDispatcher;
protected function getEventDispatcher(): EventDispatcherInterface
{
if (!$this->eventDispatcher) {
$this->eventDispatcher = ServiceContainer::getService('event_dispatcher');
}
return $this->eventDispatcher;
}
private ?ErrorIgnoringEventDispatcher $errorIgnoringEventDispatcher = null;
protected function getErrorIgnoringEventDispatcher(): ErrorIgnoringEventDispatcher
{
if (!$this->errorIgnoringEventDispatcher) {
$this->errorIgnoringEventDispatcher = ServiceContainer::getService(ErrorIgnoringEventDispatcher::class);
}
return $this->errorIgnoringEventDispatcher;
}
/**
* @param bool $newsletter
*/
public function setNewsletter($newsletter)
{
$this->newsletter = $newsletter;
}
protected function submitOrderRegister()
{
$userContext = Contexts::get(UserContext::class);
if ($this->register && $userContext->getActiveId() > 0) {
$this->register = false;
}
if ($userContext->getActiveId() > 0) {
return $userContext->getActiveId();
}
if (!$this->register) {
return null;
}
// register user
try {
/* Pokud je doprava typu point (zasilkovna, ...), tak se ukladala adresa napr. zasilkovny do udaju uzivatele, coz je blbost.
* */
if (!$this->deliveryType->getDelivery()->requiresDeliveryAddress()) {
unset($this->delivery);
$this->delivery = $this->user->delivery;
$this->user->delivery = [];
}
$id = $this->user->update();
} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
throw new \KupShop\OrderingBundle\Exception\CartValidationException(translate('userRegisterError', 'ordering'));
}
$this->user->updatePassword($this->register);
// Reload user
$this->user = User::createFromId($id);
$this->user->activateUser();
$this->requestStack->getMainRequest()?->attributes->set('gtm_registration', [
'email' => $this->user->email,
'firstname' => $this->user->name ?? '',
]);
if (findModule(\Modules::JS_SHOP)) {
$this->session->set(\KupShop\GraphQLBundle\EventListener\JsShopRefreshListener::SESSION_NAME, true);
}
return $id;
}
/**
* @return array
*/
public function getErrors($type = null)
{
if (!is_null($type) && !empty($this->errors[$type])) {
return $this->errors[$type];
}
return $this->errors;
}
/**
* @param $product Product
*
* @return int
*/
protected function roundPieces($product, $pieces)
{
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
$UnitsRounder = ServiceContainer::getService(\KupShop\UnitsBundle\Utils\PiecesRounder::class);
return $UnitsRounder->roundPieces($product, $pieces);
}
return intval($pieces);
}
private function kibanaLog(string $message, array $data = []): void
{
if (!isProduction()) {
return;
}
$logger = ServiceContainer::getService('logger');
$logger->notice($message, array_merge(
$data, [
'referer' => $_SERVER['HTTP_REFERER'],
])
);
}
/**
* @deprecated Nesmí se volat, pouze pro Naty v HeurekaCartu, kde ví co dělá. Ostatní to nevědí, tak tohle nevolají!
*/
public function markInitialized(): static
{
$this->initialized = true;
return $this;
}
#[Required]
public function setRequestStack(RequestStack $requestStack): void
{
$this->requestStack = $requestStack;
}
}
if (empty($subclass)) {
class Cart extends CartBase
{
}
}