<?php namespace QUI\ERP\Coupons; use DateTime; use Exception; use QUI; use QUI\ERP\Discount\Handler as DiscountHandler; use QUI\ERP\Order\AbstractOrder; use QUI\ERP\Order\OrderInterface; use QUI\ExceptionStack; use QUI\Interfaces\Users\User; use QUI\Permissions\Permission; use function array_key_first; use function count; use function current; use function in_array; use function is_array; use function is_null; use function is_numeric; use function json_decode; use function json_encode; use function method_exists; /** * Class CouponCode */ class CouponCode { /** * CouponCode ID * * @var int */ protected int $id; /** * Actual code * * @var string */ protected mixed $code; /** * IDs of users that this CouponCode is restricted to * * @var int[] */ protected mixed $userIds = []; /** * IDs of groups that this CouponCode is restricted to * * @var int[] */ protected mixed $groupIds = []; /** * IDs of all linked discounts * * @var array */ protected array $discountIds = []; /** * List of usages of this CouponCode * * @var array */ protected mixed $usages = []; /** * Creation Date * * @var DateTime|null */ protected ?DateTime $CreateDate = null; /** * Date until the CouponCode is valid * * @var DateTime|null */ protected ?DateTime $ValidUntilDate = null; /** * CouponCode title * * @var string|null */ protected ?string $title = null; /** * Flag - Is the CouponCode valid? * * @var bool */ protected bool $valid = true; /** * Max usages * * @var string */ protected string $maxUsages = Handler::MAX_USAGE_ONCE_PER_USER; /** * CouponCode constructor. * * @param int $id - Invite Code ID * * @throws CouponCodeException * @throws Exception */ public function __construct(int $id) { try { $result = QUI::getDataBase()->fetch([ 'from' => Handler::getTable(), 'where' => [ 'id' => $id ] ]); } catch (QUI\Database\Exception $Exception) { QUI\System\Log::addError($Exception->getMessage()); throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.not_found', [ 'id' => $id ] ], 404); } if (empty($result)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.not_found', [ 'id' => $id ] ], 404); } $data = current($result); $this->id = (int)$data['id']; $this->code = $data['code']; $this->title = $data['title']; if (!empty($data['usages'])) { $this->usages = json_decode($data['usages'], true); } if (!empty($data['userIds'])) { $this->userIds = json_decode($data['userIds'], true); } // migrate user ids foreach ($this->userIds as $k => $userId) { if (is_numeric($userId)) { try { $this->userIds[$k] = QUI::getUsers()->get($userId)->getUUID(); } catch (QUI\Exception) { } } } if (!empty($data['groupIds'])) { $this->groupIds = json_decode($data['groupIds'], true); } // migrate user ids foreach ($this->groupIds as $k => $groupId) { if (is_numeric($groupId)) { try { $this->groupIds[$k] = QUI::getGroups()->get($groupId)->getUUID(); } catch (QUI\Exception) { } } } if (!empty($data['maxUsages'])) { $this->maxUsages = $data['maxUsages']; } if (!empty($data['discountIds'])) { $this->discountIds = json_decode($data['discountIds'], true); } $this->CreateDate = new DateTime($data['createDate']); if (!empty($data['validUntilDate'])) { $this->ValidUntilDate = new DateTime($data['validUntilDate']); $this->ValidUntilDate->setTime(23, 59, 59); } $this->checkValidity(); } /** * @return int */ public function getId(): int { return $this->id; } /** * @return string */ public function getCode(): string { return $this->code; } /** * @return DateTime */ public function getCreateDate(): DateTime { return $this->CreateDate; } /** * Get usage data * * @return array */ public function getUsages(): array { return $this->usages; } /** * @return DateTime|null */ public function getValidUntilDate(): ?DateTime { return $this->ValidUntilDate; } /** * @return string|null */ public function getTitle(): ?string { return $this->title; } /** * @return int[] */ public function getDiscountIds(): array { return $this->discountIds; } /** * Get all discounts associated with this CouponCode * * @return QUI\ERP\Discount\Discount[] */ public function getDiscounts(): array { $discounts = []; $DiscountHandler = DiscountHandler::getInstance(); foreach ($this->discountIds as $discountId) { try { $discounts[] = $DiscountHandler->getChild($discountId); } catch (Exception $Exception) { QUI\System\Log::writeDebugException($Exception); } } return $discounts; } /** * @return int[] */ public function getUserIds(): array { return $this->userIds; } /** * @return int[] */ public function getGroupIds(): array { return $this->groupIds; } /** * Redeems this CouponCode * * Hint: This may invalidate the code for future use * * @param User|null $User - The user that redeems the CouponCode [if omitted use Session User] * @param AbstractOrder|null $Order (optional) - Link redemption to a specific Order * @return void * @throws CouponCodeException * @throws QUI\Database\Exception * @throws ExceptionStack */ public function redeem(QUI\Interfaces\Users\User $User = null, AbstractOrder $Order = null): void { if (is_null($User)) { $User = QUI::getUserBySession(); } $this->checkRedemption($User); $Now = new DateTime(); $usage = [ 'userId' => $User->getUUID(), 'date' => $Now->format('Y-m-d H:i:s'), 'orderPrefixedId' => false ]; if ($Order instanceof QUI\ERP\Order\Order) { $usage['orderPrefixedId'] = $Order->getPrefixedNumber(); } $this->usages[] = $usage; QUI::getDataBase()->update( Handler::getTable(), ['usages' => json_encode($this->usages)], ['id' => $this->id] ); $this->checkValidity(); QUI::getEvents()->fireEvent( 'quiqqerCouponsRedeem', [ 'User' => $User, 'CouponCode' => $this ] ); } /** * Check if the given User can redeem this CouponCode * * @param QUI\Interfaces\Users\User $User - If omitted, use session user * @return void * @throws CouponCodeException - Thrown if not redeemable by the given User */ public function checkRedemption(QUI\Interfaces\Users\User $User): void { if (!$this->isValid()) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.no_longer_valid' ]); } $DiscountHandler = DiscountHandler::getInstance(); $discountsValid = false; $discountError = false; foreach ($this->discountIds as $discountId) { try { /** @var QUI\ERP\Discount\Discount $Discount */ $Discount = $DiscountHandler->getChild($discountId); } catch (Exception $Exception) { $discountError = $Exception->getMessage(); continue; } if ($Discount->canUsedBy($User)) { $discountsValid = true; break; } } if (!$discountsValid) { if (count($this->discountIds) === 1) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.discount_invalid', [ 'reason' => $discountError ] ]); } else { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.discounts_invalid' ]); } } // Max usage restrictions switch ($this->maxUsages) { case Handler::MAX_USAGE_ONCE_PER_USER: if ($this->hasUserRedeemed($User)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.already_used' ]); } break; case Handler::MAX_USAGE_ONCE: if (!empty($this->usages)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.already_used' ]); } break; } // Restriction to QUIQQER user(s) if (!empty($this->userIds)) { if (in_array($User->getUUID(), $this->userIds)) { if ( $this->maxUsages !== Handler::MAX_USAGE_UNLIMITED && $this->hasUserRedeemed($User) ) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.already_used' ]); } } else { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.user_not_allowed' ]); } } // Restriction to QUIQQER group(s) if (!empty($this->groupIds)) { $userInGroup = false; foreach ($this->groupIds as $groupId) { if ($User->isInGroup($groupId)) { $userInGroup = true; break; } } if ($userInGroup) { if ( $this->maxUsages !== Handler::MAX_USAGE_UNLIMITED && $this->hasUserRedeemed($User) ) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.already_used' ]); } } else { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.user_not_allowed_group' ]); } } } /** * Check if the given Order can redeem this CouponCode * * @param OrderInterface|null $Order * @throws CouponCodeException */ public function checkOrderRedemption(?OrderInterface $Order): void { if ($Order === null) { return; } $DiscountHandler = DiscountHandler::getInstance(); $discountsValid = false; $discountError = false; foreach ($this->discountIds as $discountId) { try { /** @var QUI\ERP\Discount\Discount $Discount */ $Discount = $DiscountHandler->getChild($discountId); } catch (Exception $Exception) { $discountError = $Exception->getMessage(); continue; } if ($Discount->canUsedInOrder($Order)) { $discountsValid = true; break; } } if (!$discountsValid) { if (count($this->discountIds) === 1) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.discount_invalid', [ 'reason' => $discountError ] ]); } else { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.CouponCode.discounts_invalid' ]); } } } /** * Check if the given User can redeem this CouponCode * * @param User|null $User - If omitted, use session user * @param OrderInterface|null $Order * @return bool */ public function isRedeemable(QUI\Interfaces\Users\User $User = null, OrderInterface $Order = null): bool { try { $this->checkRedemption($User); } catch (CouponCodeException) { return false; } if ($Order) { try { $this->checkOrderRedemption($Order); } catch (CouponCodeException $Exception) { $Order->addFrontendMessage($Exception->getMessage()); return false; } } return true; } /** * Check if this CouponCode is still valid * * @return bool */ public function isValid(): bool { return $this->valid; } /** * Checks if an CouponCode has been redeemed by a user * * @param QUI\Interfaces\Users\User $User * @return bool */ public function hasUserRedeemed(QUI\Interfaces\Users\User $User): bool { $userId = $User->getUUID(); foreach ($this->usages as $usage) { if ($usage['userId'] === $userId) { return true; } } return false; } /** * Permanently delete this CouponCode * * @return void * @throws QUI\Permissions\Exception */ public function delete(): void { Permission::checkPermission(Handler::PERMISSION_DELETE); try { QUI::getDataBase()->delete( Handler::getTable(), ['id' => $this->id] ); } catch (QUI\Database\Exception $Exception) { QUI\System\Log::addError($Exception->getMessage()); } // If hidden discount are connected to this coupon -> delete them as well foreach ($this->getDiscounts() as $Discount) { if (!empty($Discount->getAttribute('hidden'))) { try { $Discount->delete(); } catch (Exception $Exception) { QUI\System\Log::writeException($Exception); } } } } /** * Get CouponCode attributes as array * * @return array */ public function toArray(): array { $data = [ 'id' => $this->getId(), 'code' => $this->getCode(), 'userIds' => $this->userIds, 'groupIds' => $this->groupIds, 'createDate' => $this->getCreateDate()->format('Y-m-d H:i:s'), 'usages' => $this->usages, 'validUntilDate' => false, 'title' => $this->getTitle() ?: false, 'isValid' => $this->isValid(), 'maxUsages' => $this->maxUsages, 'discountIds' => $this->discountIds ]; $ValidUntilDate = $this->getValidUntilDate(); if ($ValidUntilDate) { $data['validUntilDate'] = $ValidUntilDate->format('Y-m-d'); } return $data; } /** * Checks if this CouponCode is still valid * * @return void */ protected function checkValidity(): void { // Check if the expiration date has been reached if (!empty($this->ValidUntilDate)) { $Now = new DateTime(); if ($Now > $this->ValidUntilDate) { $this->valid = false; return; } } if ($this->maxUsages === Handler::MAX_USAGE_UNLIMITED) { return; } if ($this->maxUsages === Handler::MAX_USAGE_ONCE && !empty($this->usages)) { $this->valid = false; return; } // If the CouponCode is restricted to certain users -> Check if all those // users have already redeemed the code if (!empty($this->userIds)) { $usedByAllUsers = true; foreach ($this->userIds as $userId) { foreach ($this->usages as $usage) { if ($userId == $usage['userId']) { continue 2; } } $usedByAllUsers = false; break; } if ($usedByAllUsers) { $this->valid = false; } } } /** * @param QUI\ERP\Order\OrderInProcess $Order * @throws QUI\Exception */ public function addToOrder(QUI\ERP\Order\OrderInProcess $Order): void { $coupons = $Order->getDataEntry('quiqqer-coupons'); if (!$coupons) { return; } if (!is_array($coupons)) { return; } $priceFactors = []; $articles = []; $calculations = $Order->getArticles()->getCalculations(); $vatArray = $calculations['vatArray']; $vat = false; if (count($vatArray) === 1) { $vat = array_key_first($vatArray); } foreach ($coupons as $coupon) { /* @var $Coupon CouponCode */ try { $Coupon = Handler::getCouponCodeByCode($coupon); } catch (Exception) { continue; } // coupon check if (!$Coupon->isRedeemable($Order->getCustomer(), $Order)) { continue; } /* @var $Discount QUI\ERP\Discount\Discount */ $discounts = $Coupon->getDiscounts(); foreach ($discounts as $Discount) { $PriceFactor = $Discount->toPriceFactor(null, $Order->getCustomer()); if ($vat !== false) { $PriceFactor->setVat($vat); } $PriceFactor->setTitle( QUI::getLocale()->get('quiqqer/coupons', 'coupon.discount.title', [ 'code' => $Coupon->getCode() ]) ); $priceFactors[] = $PriceFactor; // @todo wenn fest preis (zb 10$), dann eigener produkt typ hinzufügen $articles[] = new QUI\ERP\Accounting\Articles\Text([ 'id' => -1, 'articleNo' => $Coupon->getCode(), 'title' => $PriceFactor->getTitle(), 'description' => '', 'unitPrice' => 0, 'control' => '', 'quantity' => 1, 'customData' => [ 'package' => 'quiqqer/coupon', 'code' => $Coupon->getCode() ] ]); } } if (empty($priceFactors)) { return; } /** * @param $Article * @return boolean */ $isInArticles = function ($Article) use ($Order) { $articles = $Order->getArticles(); $code = $Article->getCustomData()['code']; foreach ($articles as $Entry) { if (!method_exists($Entry, 'getCustomData')) { continue; } $customData = $Entry->getCustomData(); if (!$customData || !is_array($customData)) { continue; } if (!isset($customData['package']) || !isset($customData['code'])) { continue; } return $customData['package'] === 'quiqqer/coupon' && $customData['code'] === $code; } return false; }; foreach ($articles as $Article) { /* @var $PriceFactor QUI\ERP\Accounting\Articles\Text */ if ($isInArticles($Article) === false) { $Order->addArticle($Article); } } $Order->update(); $Order->addPriceFactors($priceFactors); } }