Skip to content
Code-Schnipsel Gruppen Projekte
Handler.php 14,8 KiB
Newer Older
Patrick Müller's avatar
Patrick Müller committed
<?php

namespace QUI\ERP\Coupons;

use DateInterval;
use DateTime;
use Exception;
use PDO;
Patrick Müller's avatar
Patrick Müller committed
use QUI;
use QUI\ERP\Discount\Handler as DiscountHandler;
Patrick Müller's avatar
Patrick Müller committed
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;
Patrick Müller's avatar
Patrick Müller committed

/**
 * Class Handler
 *
 * Main CouponCode Code handler
 */
class Handler
{
    /**
     * Permissions
     */
    const PERMISSION_VIEW = 'quiqqer.couponcode.view';
Patrick Müller's avatar
Patrick Müller committed
    const PERMISSION_CREATE = 'quiqqer.couponcode.create';
    const PERMISSION_EDIT = 'quiqqer.couponcode.edit';
Patrick Müller's avatar
Patrick Müller committed
    const PERMISSION_DELETE = 'quiqqer.couponcode.delete';
Patrick Müller's avatar
Patrick Müller committed

    /**
     * Valur for `maxUsage`
     */
    const MAX_USAGE_ONCE_PER_USER = 'oncePerUser';
    const MAX_USAGE_ONCE = 'once';
    const MAX_USAGE_UNLIMITED = 'unlimited';
Patrick Müller's avatar
Patrick Müller committed
    /**
Patrick Müller's avatar
Patrick Müller committed
     * CouponCode runtime cache
Patrick Müller's avatar
Patrick Müller committed
     *
Patrick Müller's avatar
Patrick Müller committed
     * @var CouponCode[]
Patrick Müller's avatar
Patrick Müller committed
     */
    protected static array $couponCodes = [];
Patrick Müller's avatar
Patrick Müller committed

    /**
Patrick Müller's avatar
Patrick Müller committed
     * Get CouponCode
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param int $id
Patrick Müller's avatar
Patrick Müller committed
     * @return CouponCode
     * @throws CouponCodeException
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function getCouponCode(int $id): CouponCode
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        if (isset(self::$couponCodes[$id])) {
            return self::$couponCodes[$id];
Patrick Müller's avatar
Patrick Müller committed
        }

Patrick Müller's avatar
Patrick Müller committed
        self::$couponCodes[$id] = new CouponCode($id);
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
        return self::$couponCodes[$id];
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
Patrick Müller's avatar
Patrick Müller committed
     * Get CouponCode by its actual code
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $code
Patrick Müller's avatar
Patrick Müller committed
     * @return CouponCode
Patrick Müller's avatar
Patrick Müller committed
     *
Patrick Müller's avatar
Patrick Müller committed
     * @throws CouponCodeException
     * @throws QUI\Database\Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function getCouponCodeByCode(string $code): CouponCode
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        $result = QUI::getDataBase()->fetch([
            'select' => [
Patrick Müller's avatar
Patrick Müller committed
                'id'
Patrick Müller's avatar
Patrick Müller committed
            ],
            'from' => self::getTable(),
            'where' => [
Patrick Müller's avatar
Patrick Müller committed
                'code' => $code
Patrick Müller's avatar
Patrick Müller committed
            ],
            'limit' => 1
Patrick Müller's avatar
Patrick Müller committed
        ]);
Patrick Müller's avatar
Patrick Müller committed

        if (empty($result)) {
Patrick Müller's avatar
Patrick Müller committed
            throw new CouponCodeException([
                'quiqqer/coupons',
Patrick Müller's avatar
Patrick Müller committed
                'exception.Handler.code_not_found',
Patrick Müller's avatar
Patrick Müller committed
                [
Patrick Müller's avatar
Patrick Müller committed
                    'code' => $code
Patrick Müller's avatar
Patrick Müller committed
                ]
            ], 404);
Patrick Müller's avatar
Patrick Müller committed
        }

Patrick Müller's avatar
Patrick Müller committed
        return self::getCouponCode($result[0]['id']);
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
Patrick Müller's avatar
Patrick Müller committed
     * Create new CouponCode
Patrick Müller's avatar
Patrick Müller committed
     *
     * @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
Patrick Müller's avatar
Patrick Müller committed
     * @return CouponCode
Patrick Müller's avatar
Patrick Müller committed
     *
     * @throws Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function createCouponCode(array $discountIds, array $settings = []): CouponCode
Patrick Müller's avatar
Patrick Müller committed
    {
        $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();
Patrick Müller's avatar
Patrick Müller committed

        if (!empty($settings['code'])) {
            if (self::existsCode($settings['code'])) {
Patrick Müller's avatar
Patrick Müller committed
                throw new CouponCodeException([
                    'quiqqer/coupons',
                    'exception.Handler.code_already_exists',
                    [
                        'code' => $settings['code']
Patrick Müller's avatar
Patrick Müller committed
            $code = self::sanitizeCode($settings['code']);
Patrick Müller's avatar
Patrick Müller committed
        } else {
            $code = CodeGenerator::generate();
        }

        if (empty($settings['maxUsages'])) {
            $maxUsages = self::MAX_USAGE_ONCE_PER_USER;
        } else {
            $maxUsages = $settings['maxUsages'];
        }

Patrick Müller's avatar
Patrick Müller committed
        $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)
Patrick Müller's avatar
Patrick Müller committed
        ];
Patrick Müller's avatar
Patrick Müller committed

        if (!empty($settings['validUntilDate'])) {
            $ValidUntil = new DateTime($settings['validUntilDate']);
Patrick Müller's avatar
Patrick Müller committed
            $couponCode['validUntilDate'] = $ValidUntil->format('Y-m-d H:i:s');
Patrick Müller's avatar
Patrick Müller committed
        }

        if (!empty($settings['userIds'])) {
            $couponCode['userIds'] = json_encode(explode(",", $settings['userIds']));
Patrick Müller's avatar
Patrick Müller committed
        }
Patrick Müller's avatar
Patrick Müller committed

        if (!empty($settings['groupIds'])) {
            $couponCode['groupIds'] = json_encode(explode(",", $settings['groupIds']));
Patrick Müller's avatar
Patrick Müller committed
        }

        try {
            QUI::getDataBase()->insert(
                self::getTable(),
                $couponCode
            );
        } catch (QUI\Database\Exception $e) {
            throw new CouponCodeException([
                $e->getMessage(),
                $e->getCode()
            ]);
        }
Patrick Müller's avatar
Patrick Müller committed

        return self::getCouponCode((int)QUI::getPDO()->lastInsertId());
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
     * 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']
                    ]
                ]);
            }

Patrick Müller's avatar
Patrick Müller committed
            $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);
    }

Patrick Müller's avatar
Patrick Müller committed
    /**
Patrick Müller's avatar
Patrick Müller committed
     * Search CouponCodes
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param array $searchParams
     * @param bool $countOnly (optional) - get result count only [default: false]
Patrick Müller's avatar
Patrick Müller committed
     * @return CouponCode[]|int
     * @throws CouponCodeException|QUI\Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function search(array $searchParams, bool $countOnly = false): array | int
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        $couponCodes = [];
        $Grid = new Grid($searchParams);
        $gridParams = $Grid->parseDBParams($searchParams);
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
        $binds = [];
        $where = [];
Patrick Müller's avatar
Patrick Müller committed

        if ($countOnly) {
            $sql = "SELECT COUNT(*)";
        } else {
            $sql = "SELECT id";
        }

        $sql .= " FROM `" . self::getTable() . "`";
Patrick Müller's avatar
Patrick Müller committed

        if (!empty($searchParams['search'])) {
Patrick Müller's avatar
Patrick Müller committed
            $searchColumns = [
Patrick Müller's avatar
Patrick Müller committed
                'id',
                'code',
                'email'
Patrick Müller's avatar
Patrick Müller committed
            ];
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
            $whereOr = [];
Patrick Müller's avatar
Patrick Müller committed

            foreach ($searchColumns as $searchColumn) {
                $whereOr[] = '`' . $searchColumn . '` LIKE :search';
Patrick Müller's avatar
Patrick Müller committed
            }

Henning Leutz's avatar
Henning Leutz committed
            $where[] = '(' . implode(' OR ', $whereOr) . ')';
Patrick Müller's avatar
Patrick Müller committed

Henning Leutz's avatar
Henning Leutz committed
            $binds['search'] = [
                'value' => '%' . $searchParams['search'] . '%',
                'type' => PDO::PARAM_STR
            ];
Patrick Müller's avatar
Patrick Müller committed
        }

        // build WHERE query string
        if (!empty($where)) {
            $sql .= " WHERE " . implode(" AND ", $where);
Patrick Müller's avatar
Patrick Müller committed
        }

        // ORDER
        if (!empty($searchParams['sortOn'])) {
Patrick Müller's avatar
Patrick Müller committed
            $sortOn = Orthos::clear($searchParams['sortOn']);
            $order = "ORDER BY " . $sortOn;
Patrick Müller's avatar
Patrick Müller committed

            if (!empty($searchParams['sortBy'])) {
                $order .= " " . Orthos::clear($searchParams['sortBy']);
Patrick Müller's avatar
Patrick Müller committed
            } else {
                $order .= " ASC";
            }

            $sql .= " " . $order;
Patrick Müller's avatar
Patrick Müller committed
        } else {
            $sql .= " ORDER BY id DESC";
        }

        // LIMIT
        if (!empty($gridParams['limit']) && !$countOnly) {
            $sql .= " LIMIT " . $gridParams['limit'];
Patrick Müller's avatar
Patrick Müller committed
        } else {
            if (!$countOnly) {
                $sql .= " LIMIT " . 20;
Patrick Müller's avatar
Patrick Müller committed
            }
        }

        $Stmt = QUI::getPDO()->prepare($sql);

        // bind search values
        foreach ($binds as $var => $bind) {
            $Stmt->bindValue(':' . $var, $bind['value'], $bind['type']);
Patrick Müller's avatar
Patrick Müller committed
        }

        try {
            $Stmt->execute();
            $result = $Stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Exception $Exception) {
Patrick Müller's avatar
Patrick Müller committed
            QUI\System\Log::addError(
                self::class . ' :: search() -> ' . $Exception->getMessage()
Patrick Müller's avatar
Patrick Müller committed
            );

Patrick Müller's avatar
Patrick Müller committed
            return [];
Patrick Müller's avatar
Patrick Müller committed
        }

        if ($countOnly) {
            return (int)current(current($result));
Patrick Müller's avatar
Patrick Müller committed
        }

        foreach ($result as $row) {
Patrick Müller's avatar
Patrick Müller committed
            $couponCodes[] = self::getCouponCode($row['id']);
Patrick Müller's avatar
Patrick Müller committed
        }

Patrick Müller's avatar
Patrick Müller committed
        return $couponCodes;
Patrick Müller's avatar
Patrick Müller committed
    }

    /**
     * Check if a CouponCode exists by its code
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param string $code The code of the CouponCode
     * @return bool Returns true if the CouponCode exists, false otherwise
     *
     * @throws QUI\Database\Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function existsCode(string $code): bool
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        $result = QUI::getDataBase()->fetch([
Patrick Müller's avatar
Patrick Müller committed
            'select' => 'id',
            'from' => self::getTable(),
            'where' => [
Patrick Müller's avatar
Patrick Müller committed
                'code' => $code
Patrick Müller's avatar
Patrick Müller committed
            ],
            'limit' => 1
Patrick Müller's avatar
Patrick Müller committed
        ]);
Patrick Müller's avatar
Patrick Müller committed

        return !empty($result);
    }

    /**
     * Get Registration site
     *
     * @return QUI\Projects\Site|false
     */
    public static function getRegistrationSite(): bool | QUI\Projects\Site
Patrick Müller's avatar
Patrick Müller committed
    {
            $Conf = QUI::getPackage('quiqqer/coupons')->getConfig();
            $regSite = $Conf->get('settings', 'registrationSite');
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeDebugException($Exception);

            return false;
        }
Patrick Müller's avatar
Patrick Müller committed

        if (empty($regSite)) {
            return false;
        }

        try {
            return QUI\Projects\Site\Utils::getSiteByLink($regSite);
Patrick Müller's avatar
Patrick Müller committed
            return false;
        }
    }

    /**
Patrick Müller's avatar
Patrick Müller committed
     * Deletes all CouponCodes that are expired
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param int|null $days (optional) - Delete expired Codes that are older than X days [default: delete all]
Patrick Müller's avatar
Patrick Müller committed
     * @return void
     *
     * @throws Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function deleteExpiredCouponCodes(null | int $days = null): void
Patrick Müller's avatar
Patrick Müller committed
    {
        $Now = new DateTime();
Patrick Müller's avatar
Patrick Müller committed
        $where = [
            'validUntilDate' => [
                'type' => '<=',
Patrick Müller's avatar
Patrick Müller committed
                'value' => $Now->format('Y-m-d H:i:s')
Patrick Müller's avatar
Patrick Müller committed
            ]
        ];
Patrick Müller's avatar
Patrick Müller committed

        if (!is_null($days)) {
            $OldDate = new DateTime();
            $OldDate->sub(new DateInterval('P' . $days . 'D'));
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
            $where['validUntilDate'] = [
                'type' => '<=',
Patrick Müller's avatar
Patrick Müller committed
                'value' => $OldDate->format('Y-m-d H:i:s')
Patrick Müller's avatar
Patrick Müller committed
            ];
Patrick Müller's avatar
Patrick Müller committed
        }

        QUI::getDataBase()->delete(
            self::getTable(),
            $where
        );
    }

    /**
Patrick Müller's avatar
Patrick Müller committed
     * Deletes all CouponCodes that have been redeemed
Patrick Müller's avatar
Patrick Müller committed
     *
     * @param int|null $days (optional) - Delete redeemed Codes that are older than X days [default: delete all]
Patrick Müller's avatar
Patrick Müller committed
     * @return void
     *
     * @throws Exception
Patrick Müller's avatar
Patrick Müller committed
     */
    public static function deleteRedeemedCouponCodes(null | int $days = null): void
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        $where = [
            'useDate' => [
                'type' => 'NOT',
Patrick Müller's avatar
Patrick Müller committed
                'value' => null
Patrick Müller's avatar
Patrick Müller committed
            ]
        ];
Patrick Müller's avatar
Patrick Müller committed

        if (!is_null($days)) {
            $OldDate = new DateTime();
            $OldDate->sub(new DateInterval('P' . $days . 'D'));
Patrick Müller's avatar
Patrick Müller committed

Patrick Müller's avatar
Patrick Müller committed
            $where['useDate'] = [
                'type' => '<=',
Patrick Müller's avatar
Patrick Müller committed
                'value' => $OldDate->format('Y-m-d H:i:s')
Patrick Müller's avatar
Patrick Müller committed
            ];
Patrick Müller's avatar
Patrick Müller committed
        }

        QUI::getDataBase()->delete(
            self::getTable(),
            $where
        );
    }

Patrick Müller's avatar
Patrick Müller committed
    /**
     * Sanitize coupon code and allow only certain characters
     *
     * @param string $code
     * @return string
     */
    public static function sanitizeCode(string $code): string
Patrick Müller's avatar
Patrick Müller committed
    {
        return preg_replace('#[^A-Za-z0-9\.\-_\*&$% ]#i', '', $code);
Patrick Müller's avatar
Patrick Müller committed
    /**
Patrick Müller's avatar
Patrick Müller committed
     * Get CouponCode table
Patrick Müller's avatar
Patrick Müller committed
     *
     * @return string
     */
    public static function getTable(): string
Patrick Müller's avatar
Patrick Müller committed
    {
Patrick Müller's avatar
Patrick Müller committed
        return QUI::getDBTableName('quiqqer_coupons');
Patrick Müller's avatar
Patrick Müller committed
    }
}