Files
kupshop/bundles/KupShop/PreordersBundle/Entity/UserPreorder.php
2025-08-02 16:30:27 +02:00

352 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace KupShop\PreordersBundle\Entity;
use Doctrine\DBAL\Exception;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\PreordersBundle\Util\PersistentCollection;
use KupShop\PreordersBundle\Util\UserPreorderCustomData;
use KupShop\PreordersBundle\Util\UserPreorderMessages;
use Query\Operator as Op;
use Query\Product;
use Query\QueryBuilderBase;
/**
* Represents all items a user has pre-ordered during a single pre-order date.
*/
class UserPreorder
{
public const STATUS_OPEN = 'open';
public const STATUS_RECEIVED = 'received';
public const STATUS_PARTIALLY_CLOSED = 'partiallyClosed';
public const STATUS_CLOSED = 'closed';
private ?array $items = null;
private ?\User $user = null;
private ?array $preorderDate = null;
private ?Preorder $preorder = null;
private ?UserPreorderMessages $messages = null;
private ?UserPreorderCustomData $customData = null;
public static function getAllUserPreordersQuery(): QueryBuilderBase
{
$difSubQ = sqlQueryBuilder()
->select('pi.id_user',
'pi.id_preorder_date',
'SUM(pi.pieces) - SUM(pi.pieces_sent) AS piece_diff',
'SUM(pi.pieces_sent) AS sent_sum')
->from('preorders_items', 'pi')
->groupBy('pi.id_user', 'pi.id_preorder_date');
$statusCond = <<<SQL
IF(pd.date_end >= CURDATE(), 'open',
IF (diff.sent_sum = 0, 'received',
IF (diff.piece_diff > 0, 'partiallyClosed', 'closed')
)
)
SQL;
$defaultCurrency = Contexts::get(CurrencyContext::class)->getDefaultId();
return sqlQueryBuilder()
->select('CONCAT(u.id, \'-\', pd.id) AS id',
'pd.id AS id_preorder_date',
'po.id AS id_preorder',
'u.id AS id_user',
'u.firm AS user_firm',
'u.surname AS user_surname',
'u.name AS user_name',
'po.name',
'pd.date_start',
'pd.date_end',
'pd.date_shipment',
'pd.description AS date_description',
'SUM(pi.piece_price * (pi.pieces - pi.pieces_sent)) AS price_remaining',
'SUM(pi.piece_price * pi.pieces) AS price_total',
"COALESCE(JSON_VALUE(pud.custom_data, '$.currency'), '{$defaultCurrency}') AS currency",
"{$statusCond} AS status")
->from('preorders_items', 'pi')
->leftJoin('pi', 'users', 'u', 'pi.id_user = u.id')
->leftJoin('pi', 'preorders_dates', 'pd', 'pi.id_preorder_date = pd.id')
->leftJoin('pd', 'preorders', 'po', 'pd.id_preorder = po.id')
->leftJoinSubQuery('pi', $difSubQ, 'diff', 'pd.id = diff.id_preorder_date AND u.id = diff.id_user')
->leftJoin('pi', 'preorders_users_data', 'pud', 'pi.id_user = pud.id_user AND pi.id_preorder_date = pud.id_preorder_date')
->groupBy('u.id', 'pd.id');
}
public function __construct(
public readonly int $userId,
public readonly int $preorderDateId,
) {
}
public function getUser(): \User
{
if ($this->user === null) {
$this->user = \User::createFromId($this->userId);
}
return $this->user;
}
public function getItems(bool $reFetch = false, bool $withProductPhotos = false): ?array
{
if ($this->items === null || $reFetch) {
$qb = sqlQueryBuilder()
->select(
'pi.*',
'p.vat',
'p.title AS product_title',
'pv.title AS variation_title',
'CONCAT_WS(\' \', p.title, pv.title) AS title',
'pi.piece_price * pi.pieces AS price_total',
'pi.piece_price * (pi.pieces - pi.pieces_sent) AS price_remaining',
'(SELECT EXISTS (SELECT * FROM products_variations WHERE id_product = pi.id_product)) AS has_variations',
)
->from('preorders_items', 'pi')
->leftJoin('pi', 'products', 'p', 'p.id = pi.id_product')
->leftJoin('pi', 'products_variations', 'pv', 'pv.id = pi.id_variation')
->orderBy('pi.pieces - pi.pieces_sent', 'DESC')
->where(Op::equals(['pi.id_user' => $this->userId, 'pi.id_preorder_date' => $this->preorderDateId]));
if ($withProductPhotos) {
$qb->addSelect(Product::withProductPhotoId());
}
$this->items = $qb->execute()->fetchAllAssociative();
}
return $this->items;
}
/**
* Updates item count, if item was already pre-ordered before. Or inserts if not.
*
* @return int|null id of inserted/updated/deleted preorder item
*
* @throws \Exception
* @throws Exception
*/
public function setItem(
?int $preorderItemId = null,
?int $idProduct = null,
?int $idVariation = null,
?int $count = null,
?int $piecesSent = null,
?Price $piecePrice = null,
): ?int {
$existing = null;
foreach ($this->getItems() as $item) {
$matchesProduct = $item['id_product'] == $idProduct
&& ($idVariation === null || $item['id_variation'] == $idVariation);
$matchesId = $preorderItemId == $item['id'];
if ($matchesId || $matchesProduct) {
$existing = $item;
break;
}
}
// item not yet pre-ordered
if ($existing === null) {
if ($count > 0) {
return $this->insertNewItem($idProduct, $idVariation, $piecePrice, $count);
}
return null;
}
$qb = sqlQueryBuilder()
->where(Op::equals(['pi.id' => $existing['id']]));
// remove item from preorder
if ($count === 0) {
$qb->delete('preorders_items', 'pi')
->execute();
return (int) $existing['id'];
}
$sets = [];
if ($piecePrice !== null) {
$sets['piece_price'] = $piecePrice->getPriceWithoutVat();
$sets['currency'] = $piecePrice->getCurrency()->getId();
}
if ($count !== null && $count != $existing['pieces']) {
if ($count < ($piecesSent ?? $existing['pieces_sent'])) {
$count = $piecesSent ?? $existing['pieces_sent'];
}
$sets['pieces'] = $count;
}
if ($piecesSent !== null && $piecesSent != $existing['pieces_sent']) {
if ($piecesSent > ($count ?? $existing['pieces'])) {
$piecesSent = $count ?? $existing['pieces'];
}
$sets['pieces_sent'] = $piecesSent;
}
if (!empty($sets)) {
foreach ($sets as $key => $value) {
$paramKey = ":_{$key}";
$qb->set($key, $paramKey);
$qb->addParameters([
$paramKey => $value,
]);
}
$qb->update('preorders_items', 'pi')->execute();
return (int) $existing['id'];
}
return null;
}
/**
* @throws Exception
*/
private function insertNewItem(int $idProduct, ?int $idVariation, ?Price $piecePrice, int $count = 1): int
{
$item = [
'id_user' => $this->userId,
'id_preorder_date' => $this->preorderDateId,
'id_product' => $idProduct,
'pieces' => $count,
];
if ($piecePrice !== null) {
$item['piece_price'] = $piecePrice->getPriceWithoutVat();
$item['currency'] = $piecePrice->getCurrency();
}
if ($idVariation !== null) {
$item['id_variation'] = $idVariation;
}
sqlQueryBuilder()
->insert('preorders_items')
->directValues($item)
->execute();
return (int) sqlInsertId();
}
public function getTemplateVars(): ?array
{
$spec = Op::equals(['u.id' => $this->userId, 'pi.id_preorder_date' => $this->preorderDateId]);
try {
$preorderRes = self::getAllUserPreordersQuery()
->andWhere($spec)
->execute()
->fetchAllAssociative();
$items = $this->getItems();
} catch (\Throwable) {
return null;
}
if (count($preorderRes) !== 1) {
return null;
}
return $preorderRes[0] + ['items' => $items];
}
public function getPreorderDate(): ?array
{
if ($this->preorderDate === null) {
$qb = sqlQueryBuilder()
->select('pd.*', 'po.*', 'pd.id AS id')
->from('preorders_dates', 'pd')
->leftJoin('pd', 'preorders', 'po', 'pd.id_preorder = po.id')
->where(Op::equals(['pd.id' => $this->preorderDateId]));
$found = $qb->execute()->fetchAllAssociative();
if (count($found) !== 1) {
return null;
}
$this->preorderDate = $found[0];
if (!empty($this->preorderDate['settings'])) {
$this->preorderDate['settings'] = json_decode($this->preorderDate['settings'], true);
}
if (!empty($this->preorderDate['min_price'])) {
$this->preorderDate['min_price'] = toDecimal($this->preorderDate['min_price']);
}
}
return $this->preorderDate;
}
public function getPreorder(): ?Preorder
{
if ($this->preorder === null) {
$date = $this->getPreorderDate();
if (!$date || empty($date['id_preorder'])) {
return null;
}
$this->preorder = new Preorder((int) $date['id_preorder']);
}
return $this->preorder;
}
public function getStatus(): string
{
$end = \DateTimeImmutable::createFromFormat('Y-m-d', $this->getPreorderDate()['date_end']);
if (!(new \DateTimeImmutable())->diff($end)->invert) {
return self::STATUS_OPEN;
}
$piecesSent = 0;
$piecesTotal = 0;
foreach ($this->getItems() as $item) {
$piecesSent += $item['pieces_sent'];
$piecesTotal += $item['pieces'];
}
if ($piecesTotal > 0 && $piecesSent == $piecesTotal) {
return self::STATUS_CLOSED;
}
if ($piecesTotal > 0 && $piecesSent == 0) {
return self::STATUS_RECEIVED;
}
return self::STATUS_PARTIALLY_CLOSED;
}
/**
* @return PersistentCollection<int, UserPreorderMessage>
*/
public function getMessages(): PersistentCollection
{
return $this->messages ??= new UserPreorderMessages($this);
}
/**
* @return PersistentCollection<string, mixed>
*/
public function getCustomData(): PersistentCollection
{
return $this->customData ??= new UserPreorderCustomData($this);
}
}