diff --git a/composer.json b/composer.json index a87056dda7900da79e668c650d73bbf149b1fba8..8cbc008b5fa68441de63a39a6c94e803046b0730 100644 --- a/composer.json +++ b/composer.json @@ -1,28 +1,23 @@ { "name": "quiqqer/coupons", - "type": "quiqqer-plugin", + "type": "quiqqer-module", "description": "Coupons for QUIQQER", - "version": "dev-master", - "license": "", + "version": "dev-dev", + "license": "GPL-3.0+", "authors": [ { - "name": "Henning Leutz", - "email": "leutz@pcsg.de", + "name": "Patrick Müller", + "email": "support@pcsg.de", "homepage": "http://www.pcsg.de", "role": "Developer" } ], "support": { - "email": "support@pcsg.de", - "url": "http://www.pcsg.de" - }, - "require": { - "php": ">=5.3", - "quiqqer/quiqqer": "*@dev" + "email": "support@pcsg.de" }, "autoload": { - "psr-0": { - "Hen": "src/" + "psr-4": { + "QUI\\ERP\\Coupons\\": "src/QUI/ERP/Coupons" } } } \ No newline at end of file diff --git a/locale.xml b/locale.xml new file mode 100644 index 0000000000000000000000000000000000000000..297030e27a5efe21fba7c97c9d9c527e535b2c2b --- /dev/null +++ b/locale.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<locales> + <groups name="quiqqer/coupons" datatype="php,js"> + <locale name="package.title"> + <de><![CDATA[QUIQQER ERP - Coupons]]></de> + <en><![CDATA[QUIQQER ERP - Coupons]]></en> + </locale> + <locale name="package.description"> + <de><![CDATA[Verwaltung von Coupon-Codes für die Verwendung im QUIQQER ERP Shop]]></de> + <en><![CDATA[Management of coupon codes for usage in the QUIQQER ERP Shop]]></en> + </locale> + </groups> +</locales> diff --git a/package.xml b/package.xml new file mode 100644 index 0000000000000000000000000000000000000000..25c2f4915d85e28b49fc1703aa622d911d93b2b1 --- /dev/null +++ b/package.xml @@ -0,0 +1,25 @@ +<quiqqer> + <package> + <title> + <locale group="quiqqer/coupons" var="package.title"/> + </title> + + <description> + <locale group="quiqqer/coupons" var="package.description"/> + </description> + + <support> + <email><![CDATA[support@pcsg.de]]></email> + <forum><![CDATA[https://community.quiqqer.com]]></forum> + <source><![CDATA[https://dev.quiqqer.com/quiqqer/coupons]]></source> + <issues><![CDATA[https://dev.quiqqer.com/quiqqer/coupons/issues]]></issues> + <wiki><![CDATA[https://dev.quiqqer.com/quiqqer/coupons/wikis/home]]></wiki> + </support> + + <copyright> + <name><![CDATA[PCSG - Computer & Internet Service OHG]]></name> + <license><![CDATA[GPL-3.0+]]></license> + </copyright> + + </package> +</quiqqer> \ No newline at end of file diff --git a/src/QUI/ERP/Coupons/CouponCode.php b/src/QUI/ERP/Coupons/CouponCode.php new file mode 100644 index 0000000000000000000000000000000000000000..4296cb05d59e916f345b5f641db36f3da5e5b581 --- /dev/null +++ b/src/QUI/ERP/Coupons/CouponCode.php @@ -0,0 +1,437 @@ +<?php + +namespace QUI\ERP\Coupons; + +use QUI; +use QUI\InviteCode\Exception\InviteCodeException; +use QUI\InviteCode\Exception\InviteCodeMailException; +use QUI\Permissions\Permission; + +/** + * Class InviteCode + */ +class CouponCode +{ + /** + * InviteCode ID + * + * @var int + */ + protected $id; + + /** + * Actual code + * + * @var string + */ + protected $code; + + /** + * User that is assigned to this Code + * + * @var QUI\Users\User + */ + protected $User = null; + + /** + * Email address that is asasigned to this Code + * + * @var string + */ + protected $email = null; + + /** + * Creation Date + * + * @var \DateTime + */ + protected $CreateDate; + + /** + * Use Date + * + * @var \DateTime + */ + protected $UseDate = null; + + /** + * Date until the Invite code is valid + * + * @var \DateTime + */ + protected $ValidUntilDate = null; + + /** + * InviteCode title + * + * @var string + */ + protected $title; + + /** + * Flag if mail has been sent + * + * @var bool + */ + protected $mailSent; + + /** + * @var bool + */ + protected $valid = true; + + /** + * InviteCode constructor. + * + * @param int $id - Invite Code ID + * @throws InviteCodeException + */ + public function __construct($id) + { + $result = QUI::getDataBase()->fetch(array( + 'from' => Handler::getTable(), + 'where' => array( + 'id' => $id + ) + )); + + if (empty($result)) { + throw new InviteCodeException(array( + 'quiqqer/invitecode', + 'exception.invitecode.not_found', + array( + 'id' => $id + ) + ), 404); + } + + $data = current($result); + + $this->id = (int)$data['id']; + $this->code = $data['code']; + $this->title = $data['title']; + $this->mailSent = boolval($data['mailSent']); + + if (!empty($data['userId'])) { + try { + $this->User = QUI::getUsers()->get($data['userId']); + } catch (\Exception $Exception) { +// QUI\System\Log::addWarning( +// 'Could not find User #' . $data['userId'] . ' for Invite Code #' . $this->id . '.' +// ); +// +// QUI\System\Log::writeException($Exception); + } + } + + if (!empty($data['email'])) { + $this->email = $data['email']; + } + + $this->CreateDate = new \DateTime($data['createDate']); + + if (!empty($data['useDate'])) { + $this->UseDate = new \DateTime($data['useDate']); + } + + if (!empty($data['validUntilDate'])) { + $this->ValidUntilDate = new \DateTime($data['validUntilDate']); + + $Now = new \DateTime(); + + if (!$this->isRedeemed() && $Now > $this->ValidUntilDate) { + $this->valid = false; + } + } + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @return QUI\Users\User|null + */ + public function getUser() + { + return $this->User; + } + + /** + * @param QUI\Users\User $User + */ + public function setUser(QUI\Users\User $User) + { + $this->User = $User; + } + + /** + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * @param string $email + */ + public function setEmail($email) + { + $this->email = $email; + } + + /** + * @return \DateTime + */ + public function getCreateDate() + { + return $this->CreateDate; + } + + /** + * @return \DateTime|null + */ + public function getUseDate() + { + return $this->UseDate; + } + + /** + * @return \DateTime|null + */ + public function getValidUntilDate() + { + return $this->ValidUntilDate; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return bool + */ + public function isMailSent() + { + return $this->mailSent; + } + + /** + * Redeems this InviteCode + * + * Hint: This invalidates the code for future use + * + * @param QUI\Users\User $User + * @return void + * @throws InviteCodeException + */ + public function redeem($User) + { + if ($this->isRedeemed()) { + throw new InviteCodeException(array( + 'quiqqer/invitecode', + 'exception.invitecode.already_used' + )); + } + + if (!$this->isValid()) { + throw new InviteCodeException(array( + 'quiqqer/invitecode', + 'exception.invitecode.no_longer_valid' + )); + } + + $Now = new \DateTime(); + + QUI::getDataBase()->update( + Handler::getTable(), + array( + 'useDate' => $Now->format('Y-m-d H:i:s'), + 'userId' => $User->getUniqueId() + ), + array( + 'id' => $this->id + ) + ); + + $this->UseDate = $Now; + } + + /** + * Check if this InviteCode is still valid + * + * @return bool + */ + public function isValid() + { + return $this->valid; + } + + /** + * Checks if an InviteCode is redeemed + * + * @return bool + */ + public function isRedeemed() + { + return !is_null($this->getUseDate()); + } + + /** + * Send this Invite Code via E-Mail + * + * @param bool $resend (optional) - Send again if already send [default: false] + * @return void + * @throws InviteCodeMailException + */ + public function sendViaMail($resend = false) + { + if (!$resend && $this->isMailSent()) { + return; + } + + $email = $this->getEmail(); + + if (empty($email)) { + throw new InviteCodeMailException(array( + 'quiqqer/invitecode', + 'exception.invitecode.no_email' + )); + } + + $Mailer = new QUI\Mail\Mailer(); + + $Mailer->addRecipient($email); + + $Engine = QUI::getTemplateManager()->getEngine(); + $dir = QUI::getPackage('quiqqer/invitecode')->getDir() . 'templates/'; + $validUntilDate = $this->getValidUntilDate(); + $data = array( + 'code' => $this->getCode(), + 'validUntilDate' => empty($validUntilDate) ? '' : QUI::getLocale()->formatDate($validUntilDate->getTimestamp()) + ); + + $RegistrationSite = Handler::getRegistrationSite(); + + if (empty($RegistrationSite)) { + throw new InviteCodeMailException(array( + 'quiqqer/invitecode', + 'exception.invitecode.no_registration_site' + )); + } + + $data['registrationUrl'] = $RegistrationSite->getUrlRewrittenWithHost(); + $data['email'] = $email; + $data['inviteCode'] = $this->getCode(); + + if (empty($validUntilDate)) { + $translationVar = 'mail.invite_code.body'; + } else { + $translationVar = 'mail.invite_code.body_date'; + } + + $Engine->assign(array( + 'body' => QUI::getLocale()->get( + 'quiqqer/invitecode', + $translationVar, + $data + ) + )); + + $Mailer->setSubject(QUI::getLocale()->get( + 'quiqqer/invitecode', + 'mail.invite_code.subject' + )); + + $Mailer->setBody($Engine->fetch($dir . 'mail.invite_code.html')); + $Mailer->send(); + + // update internal flag + QUI::getDataBase()->update( + Handler::getTable(), + array( + 'mailSent' => 1 + ), + array( + 'id' => $this->getId() + ) + ); + } + + /** + * Permanently delete this InviteCode + * + * @return void + */ + public function delete() + { + Permission::checkPermission(Handler::PERMISSION_DELETE); + + QUI::getDataBase()->delete( + Handler::getTable(), + array( + 'id' => $this->id + ) + ); + } + + /** + * Get InviteCode attributes as array + * + * @return array + */ + public function toArray() + { + $data = array( + 'id' => $this->getId(), + 'code' => $this->getCode(), + 'userId' => false, + 'username' => false, + 'email' => $this->getEmail() ?: false, + 'createDate' => $this->getCreateDate()->format('Y-m-d H:i:s'), + 'useDate' => false, + 'validUntilDate' => false, + 'title' => $this->getTitle() ?: false, + 'mailSent' => $this->isMailSent(), + 'valid' => $this->isValid() + ); + + $User = $this->getUser(); + + if ($User) { + $data['userId'] = $User->getId(); + $data['username'] = $User->getName(); + } + + $UseDate = $this->getUseDate(); + + if ($UseDate) { + $data['useDate'] = $UseDate->format('Y-m-d H:i:s'); + } + + $ValidUntilDate = $this->getValidUntilDate(); + + if ($ValidUntilDate) { + $data['validUntilDate'] = $ValidUntilDate->format('Y-m-d H:i:s'); + } + + return $data; + } +} diff --git a/src/QUI/ERP/Coupons/Handler.php b/src/QUI/ERP/Coupons/Handler.php new file mode 100644 index 0000000000000000000000000000000000000000..e2b97662641239071157e8a99018c4a73717cffa --- /dev/null +++ b/src/QUI/ERP/Coupons/Handler.php @@ -0,0 +1,364 @@ +<?php + +namespace QUI\ERP\Coupons; + +use QUI; +use QUI\Utils\Grid; +use QUI\Utils\Security\Orthos; +use QUI\InviteCode\Exception\InviteCodeException; + +/** + * Class Handler + * + * Main CouponCode Code handler + */ +class Handler +{ + /** + * Permissions + */ + const PERMISSION_VIEW = 'quiqqer.invitecode.view'; + const PERMISSION_CREATE = 'quiqqer.invitecode.create'; + const PERMISSION_SEND_MAIL = 'quiqqer.invitecode.send_mail'; + const PERMISSION_DELETE = 'quiqqer.invitecode.delete'; + + /** + * InviteCode runtime cache + * + * @var InviteCode[] + */ + protected static $inviteCodes = array(); + + /** + * Get InviteCode + * + * @param int $id + * @return InviteCode + */ + public static function getInviteCode($id) + { + if (isset(self::$inviteCodes[$id])) { + return self::$inviteCodes[$id]; + } + + self::$inviteCodes[$id] = new InviteCode($id); + + return self::$inviteCodes[$id]; + } + + /** + * Get InviteCode by its actual code + * + * @param string $code + * @return InviteCode + * + * @throws InviteCodeException + */ + public static function getInviteCodeByCode($code) + { + $result = QUI::getDataBase()->fetch(array( + 'select' => array( + 'id' + ), + 'from' => self::getTable(), + 'where' => array( + 'code' => $code + ), + 'limit' => 1 + )); + + if (empty($result)) { + throw new InviteCodeException(array( + 'quiqqer/invitecode', + 'exception.handler.code_not_found', + array( + 'code' => $code + ) + ), 404); + } + + return self::getInviteCode($result[0]['id']); + } + + /** + * Create new InviteCode + * + * @param array $data + * @return InviteCode + * + * @throws \Exception + */ + public static function createInviteCode($data) + { + $Now = new \DateTime(); + + $inviteCode = array( + 'title' => empty($data['title']) ? '' : $data['title'], + 'createDate' => $Now->format('Y-m-d H:i:s'), + 'code' => CodeGenerator::generate() + ); + + if (!empty($data['validUntil'])) { + $ValidUntil = new \DateTime($data['validUntil']); + $inviteCode['validUntilDate'] = $ValidUntil->format('Y-m-d H:i:s'); + } + + if (!empty($data['email'])) { + try { + QUI::getUsers()->getUserByMail($data['email']); + + throw new InviteCodeException(array( + 'quiqqer/invitecode', + 'exception.handler.user_already_exists', + array( + 'email' => $data['email'] + ) + )); + } catch (QUI\Users\Exception $Exception) { + if ($Exception->getCode() !== 404) { + throw $Exception; + } + } + + $inviteCode['email'] = trim($data['email']); + } + + QUI::getDataBase()->insert( + self::getTable(), + $inviteCode + ); + + return self::getInviteCode(QUI::getPDO()->lastInsertId()); + } + + /** + * Search InviteCodes + * + * @param array $searchParams + * @param bool $countOnly (optional) - get result count only [default: false] + * @return InviteCode[]|int + */ + public static function search($searchParams, $countOnly = false) + { + $inviteCodes = array(); + $Grid = new Grid($searchParams); + $gridParams = $Grid->parseDBParams($searchParams); + + $binds = array(); + $where = array(); + + if ($countOnly) { + $sql = "SELECT COUNT(*)"; + } else { + $sql = "SELECT id"; + } + + $sql .= " FROM `" . self::getTable() . "`"; + + if (!empty($searchParams['search'])) { + $searchColumns = array( + 'id', + 'code', + 'email' + ); + + $whereOr = array(); + + foreach ($searchColumns as $searchColumn) { + $whereOr[] = '`' . $searchColumn . '` LIKE :search'; + } + + if (!empty($whereOr)) { + $where[] = '(' . implode(' OR ', $whereOr) . ')'; + + $binds['search'] = array( + '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 (isset($searchParams['sortBy']) && + !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 " . (int)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 array(); + } + + if ($countOnly) { + return (int)current(current($result)); + } + + foreach ($result as $row) { + $inviteCodes[] = self::getInviteCode($row['id']); + } + + return $inviteCodes; + } + + /** + * Check if an invite code already eixsts + * + * @param string $code + * @return bool + */ + public static function existsCode($code) + { + $result = QUI::getDataBase()->fetch(array( + 'select' => 'id', + 'from' => self::getTable(), + 'where' => array( + 'code' => $code + ), + 'limit' => 1 + )); + + return !empty($result); + } + + /** + * Get Registration site + * + * @return QUI\Projects\Site|false + */ + public static function getRegistrationSite() + { + $Conf = QUI::getPackage('quiqqer/invitecode')->getConfig(); + $regSite = $Conf->get('settings', 'registrationSite'); + + if (empty($regSite)) { + return false; + } + + try { + return QUI\Projects\Site\Utils::getSiteByLink($regSite); + } catch (\Exception $Exception) { + return false; + } + } + + /** + * Deletes all InviteCodes that are expired + * + * @param int $days (optional) - Delete expired Codes that are older than X days [default: delete all] + * @return void + * + * @throws \Exception + */ + public static function deleteExpiredInviteCodes($days = null) + { + $Now = new \DateTime(); + $where = array( + 'validUntilDate' => array( + 'type' => '<=', + 'value' => $Now->format('Y-m-d H:i:s') + ) + ); + + if (!is_null($days)) { + $days = (int)$days; + $OldDate = new \DateTime(); + $OldDate->sub(new \DateInterval('P' . $days . 'D')); + + $where['validUntilDate'] = array( + 'type' => '<=', + 'value' => $OldDate->format('Y-m-d H:i:s') + ); + } + + QUI::getDataBase()->delete( + self::getTable(), + $where + ); + } + + /** + * Deletes all InviteCodes that have been redeemed + * + * @param int $days (optional) - Delete redeemed Codes that are older than X days [default: delete all] + * @return void + * + * @throws \Exception + */ + public static function deleteRedeemedInviteCodes($days = null) + { + $where = array( + 'useDate' => array( + 'type' => 'NOT', + 'value' => null + ) + ); + + if (!is_null($days)) { + $days = (int)$days; + $OldDate = new \DateTime(); + $OldDate->sub(new \DateInterval('P' . $days . 'D')); + + $where['useDate'] = array( + 'type' => '<=', + 'value' => $OldDate->format('Y-m-d H:i:s') + ); + } + + QUI::getDataBase()->delete( + self::getTable(), + $where + ); + } + + /** + * Get InviteCode table + * + * @return string + */ + public static function getTable() + { + return 'quiqqer_invitecodes'; + } +}