<?php namespace QUI\ERP\Coupons; use DateInterval; use DateTime; use Exception; use PDO; use QUI; use QUI\ERP\Discount\Handler as DiscountHandler; use QUI\Utils\Grid; use QUI\Utils\Security\Orthos; use function current; use function explode; use function implode; use function is_null; use function json_encode; use function preg_replace; /** * Class Handler * * Main CouponCode Code handler */ class Handler { /** * Permissions */ const PERMISSION_VIEW = 'quiqqer.couponcode.view'; const PERMISSION_CREATE = 'quiqqer.couponcode.create'; const PERMISSION_EDIT = 'quiqqer.couponcode.edit'; const PERMISSION_DELETE = 'quiqqer.couponcode.delete'; /** * Valur for `maxUsage` */ const MAX_USAGE_ONCE_PER_USER = 'oncePerUser'; const MAX_USAGE_ONCE = 'once'; const MAX_USAGE_UNLIMITED = 'unlimited'; /** * CouponCode runtime cache * * @var CouponCode[] */ protected static array $couponCodes = []; /** * Get CouponCode * * @param int $id * @return CouponCode * @throws CouponCodeException */ public static function getCouponCode(int $id): CouponCode { if (isset(self::$couponCodes[$id])) { return self::$couponCodes[$id]; } self::$couponCodes[$id] = new CouponCode($id); return self::$couponCodes[$id]; } /** * Get CouponCode by its actual code * * @param string $code * @return CouponCode * * @throws CouponCodeException * @throws QUI\Database\Exception */ public static function getCouponCodeByCode(string $code): CouponCode { $result = QUI::getDataBase()->fetch([ 'select' => [ 'id' ], 'from' => self::getTable(), 'where' => [ 'code' => $code ], 'limit' => 1 ]); if (empty($result)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.code_not_found', [ 'code' => $code ] ], 404); } return self::getCouponCode($result[0]['id']); } /** * Create new CouponCode * * @param array $discountIds - IDs of the discounts that are linked to this CouponCode * @param array $settings (optional) - If omitted a random default CouponCode is generated * @return CouponCode * * @throws Exception */ public static function createCouponCode(array $discountIds, array $settings = []): CouponCode { $DiscountHandler = DiscountHandler::getInstance(); if (empty($discountIds)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.no_discounts_linked' ]); } // check if all given discounts exist foreach ($discountIds as $discountId) { try { $DiscountHandler->getChild($discountId); } catch (Exception $Exception) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.discount_error', [ 'discountId' => $discountId, 'error' => $Exception->getMessage() ] ]); } } $Now = new DateTime(); if (!empty($settings['code'])) { if (self::existsCode($settings['code'])) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.code_already_exists', [ 'code' => $settings['code'] ] ]); } $code = self::sanitizeCode($settings['code']); } else { $code = CodeGenerator::generate(); } if (empty($settings['maxUsages'])) { $maxUsages = self::MAX_USAGE_ONCE_PER_USER; } else { $maxUsages = $settings['maxUsages']; } $couponCode = [ 'title' => empty($settings['title']) ? null : $settings['title'], 'createDate' => $Now->format('Y-m-d H:i:s'), 'code' => $code, 'maxUsages' => $maxUsages, 'discountIds' => json_encode($discountIds) ]; if (!empty($settings['validUntilDate'])) { $ValidUntil = new DateTime($settings['validUntilDate']); $couponCode['validUntilDate'] = $ValidUntil->format('Y-m-d H:i:s'); } if (!empty($settings['userIds'])) { $couponCode['userIds'] = json_encode(explode(",", $settings['userIds'])); } if (!empty($settings['groupIds'])) { $couponCode['groupIds'] = json_encode(explode(",", $settings['groupIds'])); } try { QUI::getDataBase()->insert( self::getTable(), $couponCode ); } catch (QUI\Database\Exception $e) { throw new CouponCodeException([ $e->getMessage(), $e->getCode() ]); } return self::getCouponCode(QUI::getPDO()->lastInsertId()); } /** * Edit a CouponCode * * @param int $id - CouponCode ID * @param array $discountIds - IDs of the discounts that are linked to this CouponCode * @param array $settings (optional) - If omitted a random default CouponCode is generated * @return CouponCode * * @throws Exception */ public static function editCouponCode(int $id, array $discountIds, array $settings = []): CouponCode { QUI\Permissions\Permission::checkPermission(self::PERMISSION_EDIT); // check if CouponCode exists $CouponCode = self::getCouponCode($id); // Check settings $DiscountHandler = DiscountHandler::getInstance(); if (empty($discountIds)) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.no_discounts_linked' ]); } // check if all given discounts exist foreach ($discountIds as $discountId) { try { $DiscountHandler->getChild($discountId); } catch (Exception $Exception) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.discount_error', [ 'discountId' => $discountId, 'error' => $Exception->getMessage() ] ]); } } $Now = new DateTime(); if (!empty($settings['code'])) { if ( $CouponCode->getCode() !== $settings['code'] && self::existsCode($settings['code']) ) { throw new CouponCodeException([ 'quiqqer/coupons', 'exception.Handler.code_already_exists', [ 'code' => $settings['code'] ] ]); } $code = self::sanitizeCode($settings['code']); } else { $code = CodeGenerator::generate(); } if (empty($settings['maxUsages'])) { $maxUsages = self::MAX_USAGE_ONCE_PER_USER; } else { $maxUsages = $settings['maxUsages']; } $couponCode = [ 'title' => empty($settings['title']) ? null : $settings['title'], 'createDate' => $Now->format('Y-m-d H:i:s'), 'code' => $code, 'maxUsages' => $maxUsages, 'discountIds' => json_encode($discountIds) ]; if (!empty($settings['validUntilDate'])) { $ValidUntil = new DateTime($settings['validUntilDate']); $couponCode['validUntilDate'] = $ValidUntil->format('Y-m-d H:i:s'); } else { $couponCode['validUntilDate'] = null; } if (!empty($settings['userIds'])) { $couponCode['userIds'] = json_encode(explode(",", $settings['userIds'])); } else { $couponCode['userIds'] = null; } if (!empty($settings['groupIds'])) { $couponCode['groupIds'] = json_encode(explode(",", $settings['groupIds'])); } else { $couponCode['groupIds'] = null; } QUI::getDataBase()->update( self::getTable(), $couponCode, ['id' => $id] ); return self::getCouponCode($id); } /** * Search CouponCodes * * @param array $searchParams * @param bool $countOnly (optional) - get result count only [default: false] * @return CouponCode[]|int * @throws CouponCodeException|QUI\Exception */ public static function search(array $searchParams, bool $countOnly = false): array|int { $couponCodes = []; $Grid = new Grid($searchParams); $gridParams = $Grid->parseDBParams($searchParams); $binds = []; $where = []; if ($countOnly) { $sql = "SELECT COUNT(*)"; } else { $sql = "SELECT id"; } $sql .= " FROM `" . self::getTable() . "`"; if (!empty($searchParams['search'])) { $searchColumns = [ 'id', 'code', 'email' ]; $whereOr = []; foreach ($searchColumns as $searchColumn) { $whereOr[] = '`' . $searchColumn . '` LIKE :search'; } $where[] = '(' . implode(' OR ', $whereOr) . ')'; $binds['search'] = [ 'value' => '%' . $searchParams['search'] . '%', 'type' => PDO::PARAM_STR ]; } // build WHERE query string if (!empty($where)) { $sql .= " WHERE " . implode(" AND ", $where); } // ORDER if (!empty($searchParams['sortOn'])) { $sortOn = Orthos::clear($searchParams['sortOn']); $order = "ORDER BY " . $sortOn; if (!empty($searchParams['sortBy'])) { $order .= " " . Orthos::clear($searchParams['sortBy']); } else { $order .= " ASC"; } $sql .= " " . $order; } else { $sql .= " ORDER BY id DESC"; } // LIMIT if (!empty($gridParams['limit']) && !$countOnly) { $sql .= " LIMIT " . $gridParams['limit']; } else { if (!$countOnly) { $sql .= " LIMIT " . 20; } } $Stmt = QUI::getPDO()->prepare($sql); // bind search values foreach ($binds as $var => $bind) { $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']); } try { $Stmt->execute(); $result = $Stmt->fetchAll(PDO::FETCH_ASSOC); } catch (Exception $Exception) { QUI\System\Log::addError( self::class . ' :: search() -> ' . $Exception->getMessage() ); return []; } if ($countOnly) { return (int)current(current($result)); } foreach ($result as $row) { $couponCodes[] = self::getCouponCode($row['id']); } return $couponCodes; } /** * Check if a CouponCode exists by its code * * @param string $code The code of the CouponCode * @return bool Returns true if the CouponCode exists, false otherwise * * @throws QUI\Database\Exception */ public static function existsCode(string $code): bool { $result = QUI::getDataBase()->fetch([ 'select' => 'id', 'from' => self::getTable(), 'where' => [ 'code' => $code ], 'limit' => 1 ]); return !empty($result); } /** * Get Registration site * * @return QUI\Projects\Site|false */ public static function getRegistrationSite(): bool|QUI\Projects\Site { try { $Conf = QUI::getPackage('quiqqer/coupons')->getConfig(); $regSite = $Conf->get('settings', 'registrationSite'); } catch (QUI\Exception $Exception) { QUI\System\Log::writeDebugException($Exception); return false; } if (empty($regSite)) { return false; } try { return QUI\Projects\Site\Utils::getSiteByLink($regSite); } catch (Exception) { return false; } } /** * Deletes all CouponCodes that are expired * * @param int|null $days (optional) - Delete expired Codes that are older than X days [default: delete all] * @return void * * @throws Exception */ public static function deleteExpiredCouponCodes(int $days = null): void { $Now = new DateTime(); $where = [ 'validUntilDate' => [ 'type' => '<=', 'value' => $Now->format('Y-m-d H:i:s') ] ]; if (!is_null($days)) { $OldDate = new DateTime(); $OldDate->sub(new DateInterval('P' . $days . 'D')); $where['validUntilDate'] = [ 'type' => '<=', 'value' => $OldDate->format('Y-m-d H:i:s') ]; } QUI::getDataBase()->delete( self::getTable(), $where ); } /** * Deletes all CouponCodes that have been redeemed * * @param int|null $days (optional) - Delete redeemed Codes that are older than X days [default: delete all] * @return void * * @throws Exception */ public static function deleteRedeemedCouponCodes(int $days = null): void { $where = [ 'useDate' => [ 'type' => 'NOT', 'value' => null ] ]; if (!is_null($days)) { $OldDate = new DateTime(); $OldDate->sub(new DateInterval('P' . $days . 'D')); $where['useDate'] = [ 'type' => '<=', 'value' => $OldDate->format('Y-m-d H:i:s') ]; } QUI::getDataBase()->delete( self::getTable(), $where ); } /** * Sanitize coupon code and allow only certain characters * * @param string $code * @return string */ public static function sanitizeCode(string $code): string { return preg_replace('#[^A-Za-z0-9\.\-_\*&$% ]#i', '', $code); } /** * Get CouponCode table * * @return string */ public static function getTable(): string { return QUI::getDBTableName('quiqqer_coupons'); } }