<?php

namespace mii\web;

class Cookie
{

    /**
     * @var  string  Magic salt to add to the cookie
     */
    public static $salt = null;

    /**
     * @var  integer  Number of seconds before the cookie expires
     */
    public static $expiration = 0;

    /**
     * @var  string  Restrict the path that the cookie is available to
     */
    public static $path = '/';

    /**
     * @var  string  Restrict the domain that the cookie is available to
     */
    public static $domain = null;

    /**
     * @var  boolean  Only transmit cookies over secure connections
     */
    public static $secure = false;

    /**
     * @var  boolean  Only transmit cookies over HTTP, disabling Javascript access
     */
    public static $httponly = true;

    /**
     * Gets the value of a signed cookie. Cookies without signatures will not
     * be returned. If the cookie signature is present, but invalid, the cookie
     * will be deleted.
     *
     *     // Get the "theme" cookie, or use "blue" if the cookie does not exist
     *     $theme = Cookie::get('theme', 'blue');
     *
     * @param   string $key cookie name
     * @param   mixed $default default value to return
     * @return  string
     */
    public static function get($key, $default = null) {
        if (!isset($_COOKIE[$key])) {
            // The cookie does not exist
            return $default;
        }

        // Get the cookie value
        $cookie = $_COOKIE[$key];;

        // Find the position of the split between salt and contents
        $split = strlen(static::salt($key, null));

        if (isset($cookie[$split]) && $cookie[$split] === '~') {
            // Separate the salt and the value
            list ($hash, $value) = explode('~', $cookie, 2);

            if (static::salt($key, $value) === $hash) {
                // Cookie signature is valid
                return $value;
            }

            // The cookie signature is invalid, delete it
            static::delete($key);
        }

        return $default;
    }

    /**
     * Sets a signed cookie. Note that all cookie values must be strings and no
     * automatic serialization will be performed!
     *
     *     // Set the "theme" cookie
     *     Cookie::set('theme', 'red');
     *
     * @param   string $name name of cookie
     * @param   string $value value of cookie
     * @param   integer $expiration lifetime in seconds
     * @return  boolean
     * @uses    Cookie::salt
     */
    public static function set($name, $value, $expiration = null) {
        if ($expiration === null) {
            // Use the default expiration
            $expiration = static::$expiration;
        }

        if ($expiration !== 0) {
            // The expiration is expected to be a UNIX timestamp
            $expiration += time();
        }

        // Add the salt to the cookie value
        $value = static::salt($name, $value) . '~' . $value;

        return setcookie($name, $value, $expiration, static::$path, static::$domain, static::$secure, static::$httponly);
    }

    /**
     * Deletes a cookie by making the value null and expiring it.
     *
     *     Cookie::delete('theme');
     *
     * @param   string $name cookie name
     * @return  boolean
     */
    public static function delete($name) {
        // Remove the cookie
        unset($_COOKIE[$name]);

        // Nullify the cookie and make it expire
        return setcookie($name, null, -86400, static::$path, static::$domain, static::$secure, static::$httponly);
    }

    /**
     * Generates a salt string for a cookie based on the name and value.
     *
     *     $salt = Cookie::salt('theme', 'red');
     *
     * @param   string $name name of cookie
     * @param   string $value value of cookie
     * @return  string
     */
    public static function salt($name, $value) {
        // Require a valid salt
        if (!static::$salt) {
            $config = Mii::$app->config('cookie');

            if ($config AND isset($config['salt'])) {
                static::$salt = $config['salt'];
            } else {
                throw new InvalidArgumentException(
                    'A valid cookie salt is required. Please set Cookie::$salt before calling this method.' .
                    'For more information check the documentation'
                );
            }
        }

        // Determine the user agent
        $agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : 'unknown';

        return sha1($agent . $name . $value . static::$salt);
    }
}


<?php

namespace Ekyna\Component\Commerce\Customer\EventListener;

use Ekyna\Component\Commerce\Common\Generator\NumberGeneratorInterface;
use Ekyna\Component\Commerce\Customer\Event\CustomerEvents;
use Ekyna\Component\Commerce\Customer\Model\CustomerInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Pricing\Updater\PricingUpdaterInterface;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

/**
 * Class CustomerListener
 * @package Ekyna\Component\Commerce\Customer\EventListener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class CustomerListener
{
    /**
     * @var PersistenceHelperInterface
     */
    protected $persistenceHelper;

    /**
     * @var NumberGeneratorInterface
     */
    protected $numberGenerator;

    /**
     * @var PricingUpdaterInterface
     */
    protected $pricingUpdater;

    /**
     * Constructor.
     *
     * @param PersistenceHelperInterface $persistenceHelper
     * @param NumberGeneratorInterface   $numberGenerator
     * @param PricingUpdaterInterface    $pricingUpdater
     */
    public function __construct(
        PersistenceHelperInterface $persistenceHelper,
        NumberGeneratorInterface $numberGenerator,
        PricingUpdaterInterface $pricingUpdater
    ) {
        $this->persistenceHelper = $persistenceHelper;
        $this->numberGenerator = $numberGenerator;
        $this->pricingUpdater = $pricingUpdater;
    }

    /**
     * Insert event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onInsert(ResourceEventInterface $event)
    {
        $customer = $this->getCustomerFromEvent($event);

        $changed = $this->generateNumber($customer);

        $changed |= $this->updateFromParent($customer);

        $changed |= $this->pricingUpdater->updateVatNumberSubject($customer);

        if ($changed) {
            $this->persistenceHelper->persistAndRecompute($customer);
        }
    }

    /**
     * Update event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onUpdate(ResourceEventInterface $event)
    {
        $customer = $this->getCustomerFromEvent($event);

        $changed = $this->generateNumber($customer);

        if ($this->persistenceHelper->isChanged($customer, 'parent')) {
            $changed |= $this->updateFromParent($customer);
        }

        $changed |= $this->pricingUpdater->updateVatNumberSubject($customer);

        if ($changed) {
            $this->persistenceHelper->persistAndRecompute($customer);
        }

        $hierarchyFields = ['company', 'customerGroup', 'vatNumber', 'vatDetails', 'vatValid'];
        if ($this->persistenceHelper->isChanged($customer, $hierarchyFields)) {
            $this->scheduleParentChangeEvents($customer);
        }
    }

    /**
     * "Parent has changed" event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onParentChange(ResourceEventInterface $event)
    {
        $customer = $this->getCustomerFromEvent($event);

        if ($this->updateFromParent($customer)) {
            $this->persistenceHelper->persistAndRecompute($customer, true);
        }
    }

    /**
     * Updates from parent.
     *
     * @param CustomerInterface $customer
     *
     * @return bool
     */
    protected function updateFromParent(CustomerInterface $customer)
    {
        if (!$customer->hasParent()) {
            // Make sure default invoice and delivery address exists.
            if (null === $customer->getDefaultInvoiceAddress()) {
                /** @var \Ekyna\Component\Commerce\Customer\Model\CustomerAddressInterface $address */
                if (false !== $address = $customer->getAddresses()->first()) {
                    $address->setInvoiceDefault(true);
                    $this->persistenceHelper->persistAndRecompute($address, false);
                }
            }
            if (null === $customer->getDefaultDeliveryAddress()) {
                /** @var \Ekyna\Component\Commerce\Customer\Model\CustomerAddressInterface $address */
                if (false !== $address = $customer->getAddresses()->first()) {
                    $address->setDeliveryDefault(true);
                    $this->persistenceHelper->persistAndRecompute($address, false);
                }
            }
            return false;
        }

        $parent = $customer->getParent();
        $changed = false;

        // Company
        if (empty($customer->getCompany())) {
            $company = $parent->getCompany();
            if ($company != $customer->getCompany()) {
                $customer->setCompany($company);
                $changed = true;
            }
        }

        // Customer group
        $group = $parent->getCustomerGroup();
        if ($group !== $customer->getCustomerGroup()) {
            $customer->setCustomerGroup($group);
            $changed = true;
        }

        // Clear VAT info
        if (!empty($customer->getVatNumber())) {
            $customer->setVatNumber(null);
            $changed = true;
        }
        if (!empty($customer->getVatDetails())) {
            $customer->setVatDetails([]);
            $changed = true;
        }
        if ($customer->isVatValid()) {
            $customer->setVatValid(false);
            $changed = true;
        }

        // Clear payment term
        if (null !== $customer->getPaymentTerm()) {
            $customer->setPaymentTerm(null);
            $changed = true;
        }
        // Clear outstanding
        if (0 !== $customer->getOutstandingLimit()) {
            $customer->setOutstandingLimit(0);
            $changed = true;
        }
        // TODO (?) Clear balance
        /*if (0 !== $customer->getOutstandingBalance()) {
            $customer->setOutstandingBalance(0);
            $changed = true;
        }*/

        return $changed;
    }

    /**
     * Schedules the parent change events.
     *
     * @param CustomerInterface $customer
     */
    protected function scheduleParentChangeEvents(CustomerInterface $customer)
    {
        if (!$customer->hasChildren()) {
            return;
        }

        foreach ($customer->getChildren() as $child) {
            $this->persistenceHelper->scheduleEvent(CustomerEvents::PARENT_CHANGE, $child);
        }
    }

    /**
     * Returns the customer from the event.
     *
     * @param ResourceEventInterface $event
     *
     * @return CustomerInterface
     * @throws InvalidArgumentException
     */
    protected function getCustomerFromEvent(ResourceEventInterface $event)
    {
        $resource = $event->getResource();

        if (!$resource instanceof CustomerInterface) {
            throw new InvalidArgumentException('Expected instance of ' . CustomerInterface::class);
        }

        return $resource;
    }

    /**
     * Generates the number.
     *
     * @param CustomerInterface $customer
     *
     * @return bool Whether the customer number has been generated or not.
     */
    private function generateNumber(CustomerInterface $customer)
    {
        if (0 == strlen($customer->getNumber())) {
            $this->numberGenerator->generate($customer);

            return true;
        }

        return false;
    }
}


<?php

/*
 * Copyright (C) 2015 Ashley Marando
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

namespace Marando\AstroDate;

use \Exception;
use \Marando\IAU\IAU;
use \Marando\IERS\IERS;
use \Marando\Units\Angle;
use \Marando\Units\Time;

/**
 * Represents a date/time and provides astronomy related functionality
 *
 * @property float     $era       Era, e.g. A.D or B.C.
 * @property float     $year      Year
 * @property float     $month     Month
 * @property float     $day       Day
 * @property float     $hour      Hour
 * @property float     $min       Minute
 * @property float     $sec       Second
 * @property float     $micro     Milliseconds
 * @property TimeZone  $timezone  Time zone
 * @property TimeScale $timescale Astronomical time scale, e.g. UTC, TAI, TT
 */
class AstroDate
{

    use \Marando\Units\Traits\CopyTrait,
      \Marando\AstroDate\Traits\FormatTrait,
      \Marando\AstroDate\Traits\SolsticeEquinoxTrait;

    //----------------------------------------------------------------------------
    // Constructors
    //----------------------------------------------------------------------------

    /**
     * Modified Julian day epoch (0h Nov 17, 1858)
     */
    const MJD = 2400000.5;

    /**
     * Unix epoch (0h Jan 1, 1970)
     */
    const UJD = 2440587.5;

    /**
     * 2015-Nov-16 17:07:07.120 UTC
     */
    const FORMAT_DEFAULT = 'Y-M-d H:i:s.u T';

    /**
     * A.D. 2015-Nov-03 19:43:43.180 TT
     */
    const FORMAT_JPL_HMS = 'r Y-M-d H:i:s.u T';

    /**
     * A.D. 2015-Nov-3.8223053 TT
     */
    const FORMAT_JPL = 'r Y-M-c T';

    /**
     * Monday, November 16, 2015 8:20 AM (UTC)
     */
    const FORMAT_GOOGLE = 'l, F j, Y g:i A (T)';

    /**
     * 2010 Jan. 4.0 TT
     */
    const FORMAT_EPOCH = 'Y M. c T';

    //----------------------------------------------------------------------------
    // Constructors
    //----------------------------------------------------------------------------

    /**
     * Creates a new AstroDate
     *
     * @param int       $year      Year
     * @param int       $month     Month
     * @param int       $day       Day
     * @param int       $hour      Hour
     * @param int       $min       Minutes
     * @param float     $sec       Seconds
     * @param TimeZone  $timezone  Time zone
     * @param TimeScale $timescale Astronomical time standard
     */
    public function __construct(
      $year = null,
      $month = null,
      $day = null,
      $hour = null,
      $min = null,
      $sec = null,
      $timezone = null,
      $timescale = null
    ) {
        // Set time scale, default to UTC
        $this->timescale = $timescale ? $timescale : TimeScale::UTC();

        // If time scale is not UTC... set time zone to UTC, otherwise set time zone
        if ($this->timescale != TimeScale::UTC()) {
            $this->timezone = TimeZone::UTC();
        } else {
            $this->timezone = $timezone ? $timezone : TimeZone::UTC();
        }

        // Civil date -> JD and fractional day
        $status = IAU::Dtf2d($this->timezone->name, (int)$year, (int)$month,
          (int)$day, (int)$hour, (int)$min, (float)$sec, $this->jd,
          $this->dayFrac);

        // Verify date is valid (will throw exceptions if not)
        $this->checkDate($status);

        // Add time zone offset
        if ($this->timezone != TimeZone::UTC()) {
            //$tzOffset = $this->timezone->offset($this->toJD());
            //$this->add(Time::hours($tzOffset));
        }

        // Set default format
        $this->format = static::FORMAT_DEFAULT;
    }

    // // // Static

    /**
     * Creates a new AstroDate
     *
     * @param  int       $year      Year
     * @param  int       $month     Month
     * @param  int       $day       Day
     * @param  int       $hour      Hour
     * @param  int       $min       Minutes
     * @param  float     $sec       Seconds
     * @param  TimeZone  $timezone  Time zone
     * @param  TimeScale $timescale Astronomical time standard
     *
     * @return static
     */
    public static function create(
      $year = null,
      $month = null,
      $day = null,
      $hour = null,
      $min = null,
      $sec = null,
      $timezone = null,
      $timescale = null
    ) {

        return new static($year, $month, $day, $hour, $min, $sec, $timezone,
          $timescale);
    }

    /**
     * Creates a new AstroDate from a Julian date
     *
     * @param  float|string $jd        Julian date
     * @param  TimeScale    $timescale Astronomical time standard, e.g. TAI or
     *                                 TT
     *
     * @return static
     */
    public static function jd($jd, TimeScale $timescale = null)
    {
        // JD -> Y-M-D H:M:S.m
        $t = [];
        IAU::D2dtf($timescale, 14, $jd, 0, $y, $m, $d, $t);

        // Create new instance from date/time components
        return new static($y, $m, $d, $t[0], $t[1], $t[2], null, $timescale);
    }

    /**
     * Creates a new AstroDate from a Modified Julian date
     *
     * @param  float     $mjd       Modified Julian date
     * @param  TimeScale $timescale Astronomical time standard, e.g. TAI or TT
     *
     * @return static
     */
    public static function mjd($mjd, TimeScale $timescale = null)
    {
        return static::jd($mjd + static::MJD, $timescale);
    }

    /**
     * Creates a new AstroDate from the current date and time
     *
     * @param  TimeZone|string $timezone
     *
     * @return static
     */
    public static function now($timezone = null)
    {
        // Get current time as micro unix timestamp
        $now   = explode(' ', microtime());
        $unix  = $now[1];
        $micro = Time::sec($now[0]);

        // Compoute JD from unix timestamp
        $jd = ($unix / 86400.0) + static::UJD;

        // Add timezone if none present
        if ($timezone == null) {
            $timezone = TimeZone::UTC();
        }
        if (is_string($timezone)) {
            $timezone = TimeZone::parse($timezone);
        }

        // Return the new date adding the micro portion and provided timezone
        return static::jd($jd)->add($micro)->setTimezone($timezone);
    }

    /**
     * Parses a string into a new AstroDate instance
     *
     * @param  string $datetime String date/time representation
     *
     * @return static
     */
    public static function parse($datetime)
    {
        $formats = [
            /**
             * 2010 Jan. 4.32425 TT
             * 2010-1-4.32425 TT
             * ...
             */
          '[a-zA-Z,\s]*([\+\-0-9]{1,7})[-\/\s]([a-zA-Z\.]{3,9}|[0-9]{1,2})[-\/\s]([0-9]{1,2}\.[0-9]*)\s*()()()()()([a-zA-Z]{2,3})*',
            /**
             * 2015-Nov-16 05:07:07.03223432 PM UTC
             * 2015-11-16 05:07:07.03223432 PM UTC
             * ...
             */
          '[a-zA-Z,\s]*([\+\-0-9]{1,7})[-\/\s]([a-zA-Z]{3,9}|[0-9]{1,2})[-\/\s]([0-9]{1,2})\s*([0-9]{0,2}):*([0-9]{0,2}):*([0-9]{0,2})(\.*[0-9]*)\s*(am|AM|pm|PM)*\s*([a-zA-Z]{0,3})',
        ];

        foreach ($formats as $format) {
            if (preg_match("/{$format}/", trim($datetime), $t)) {
                $y  = $t[1];
                $m  = static::monthNum($t[2]) ? static::monthNum($t[2]) : $t[2];
                $d  = intval($t[3]);
                $df = $t[3] - intval($t[3]);
                $h  = key_exists(4, $t) ? $t[4] : 0;
                $i  = key_exists(5, $t) ? $t[5] : 0;
                $s  = key_exists(6, $t) ? $t[6] : 0;
                $u  = key_exists(7, $t) ? $t[7] : 0;
                $a  = key_exists(8, $t) ? $t[8] : 0;
                $z  = key_exists(9, $t) ? $t[9] : 0;

                if (strtolower($a) == 'pm') {
                    $h += 12;
                }

                $dt = new static($y, $m, $d, $h, $i, $s);
                $dt->add(Time::sec($u));

                if ($df) {
                    $dt->add(Time::days($df));
                }

                try {
                    $dt->timezone  = TimeZone::parse($z);
                    $dt->timescale = TimeScale::UTC();
                } catch (Exception $e) {
                    $dt->timescale = TimeScale::parse($z);
                    $dt->timezone  = TimeZone::UTC();
                }

                return $dt;
            }
        }

        throw new Exception("Unable to parse date/time string {$datetime}");

        return;
// 2015-Nov-16 17:07:07.120 UTC
        $format1 = '^([\+\-]*[0-9]{1,7})-([a-zA-Z]{1,9})-([0-9]{1,2})\s([0-9]{1,2}):([0-9]{1,2}):*([0-9]{0,2})(\.*[0-9]*)\s*([a-zA-Z]*)$';
        if (preg_match("/{$format1}/", $datetime, $t)) {
            $m  = static::monthNum($t[2]);
            $dt = new static($t[1], $m, $t[3], $t[4], $t[5], $t[6]);
            $dt->add(Time::sec($t[7]));

            try {
                $dt->timezone  = TimeZone::parse($t[8]);
                $dt->timescale = TimeScale::UTC();
            } catch (Exception $e) {
                $dt->timescale = TimeScale::parse($t[8]);
                $dt->timezone  = TimeZone::UTC();
            }

            return $dt;
        }

        // 2015-1-16 17:07:07.120 UTC
        $format2 = '^([\+\-]*[0-9]{1,7})-([0-9]{1,2})-([0-9]{1,2})\s([0-9]{1,2}):([0-9]{1,2}):*([0-9]{0,2})(\.*[0-9]*)\s*([a-zA-Z]*)$';
        if (preg_match("/{$format2}/", $datetime, $t)) {
            $dt = new static($t[1], $t[2], $t[3], $t[4], $t[5], $t[6]);
            $dt->add(Time::sec($t[7]));

            try {
                $dt->timezone  = TimeZone::parse($t[8]);
                $dt->timescale = TimeScale::UTC();
            } catch (Exception $e) {
                $dt->timescale = TimeScale::parse($t[8]);
                $dt->timezone  = TimeZone::UTC();
            }

            return $dt;
        }

        throw new Exception("Unable to parse date/time string {$dt}");
    }

    /**
     * Creates a new AstroDate instance representing the Summer Solstice of the
     * provided year
     *
     * @param  int $year
     *
     * @return static
     */
    public static function solsticeSummer($year)
    {
        $jd = static::solsticeJune((int)$year, false);

        return AstroDate::jd($jd, TimeScale::TT());
    }

    /**
     * Creates a new AstroDate instance representing the Winter Solstice of the
     * provided year
     *
     * @param  int $year
     *
     * @return static
     */
    public static function solsticeWinter($year)
    {
        $jd = static::solsticeDecember((int)$year, false);

        return AstroDate::jd($jd, TimeScale::TT());
    }

    /**
     * Creates a new AstroDate instance representing the Spring (Vernal) Equinox
     * of the provided year
     *
     * @param  int $year
     *
     * @return static
     */
    public static function equinoxSpring($year)
    {
        $jd = static::equinoxMarch((int)$year, false);

        return AstroDate::jd($jd, TimeScale::TT());
    }

    /**
     * Creates a new AstroDate instance representing the Autumn Equinox of the
     * provided year
     *
     * @param  int $year
     *
     * @return static
     */
    public static function equinoxAutumn($year)
    {
        $jd = static::equinoxSeptember((int)$year, false);

        return AstroDate::jd($jd, TimeScale::TT());
    }

    //----------------------------------------------------------------------------
    // Properties
    //----------------------------------------------------------------------------

    /**
     * Julian day count (whole day) ...used as IAU 2 part JD with $dayFrac
     *
     * @var float
     */
    protected $jd;

    /**
     * Fractional day ...used as IAU 2 part JD with $jd
     *
     * @var float
     */
    protected $dayFrac;

    /**
     * Time zone of the instance
     *
     * @var TimeZone
     */
    protected $timezone;

    /**
     * The last set timezone. Used for when the last timezone is needed when
     * converting from something like TDB back to UTC
     *
     * @var TimeZone
     */
    protected $timezone0;

    /**
     * Astronomical time scale, e.g. UTC, TAI, TT
     *
     * @var TimeScale
     */
    protected $timescale;

    /**
     * Current toString format
     *
     * @var string
     */
    protected $format;

    /**
     * Decimal precision of this instance (NDP for IAU methods)
     *
     * @var int
     */
    protected $prec = 11;

    public function __get($name)
    {
        switch ($name) {
            case 'era':
                return $this->year < 1 ? 'B.C.' : 'A.D.';

            case 'year':
            case 'month':
            case 'day':
            case 'hour':
            case 'min':
            case 'sec':
            case 'micro':
                return $this->getComponent($name);

            case 'timezone':
            case 'timescale':
                return $this->{$name};

            default:
                throw new Exception("{$name} is not a valid or public property");
        }
    }

    public function __set($name, $value)
    {
        switch ($name) {
            case 'year':
                return $this->setDate($value, $this->month, $this->day);

            case 'month':
                return $this->setDate($this->year, $value, $this->day);

            case 'day':
                return $this->setDate($this->year, $this->month, $value);

            case 'hour':
                return $this->setTime($value, $this->min, $this->sec);

            case 'min':
                return $this->setTime($this->hour, $value, $this->sec);

            case 'sec':
                return $this->setTime($this->hour, $this->min, $value);
        }
    }

    //----------------------------------------------------------------------------
    // Functions
    //----------------------------------------------------------------------------

    /**
     * Sets the date for this instance
     *
     * @param  int $year  Year
     * @param  int $month Month
     * @param  int $day   Day
     *
     * @return static
     */
    public function setDate($year, $month, $day)
    {
        $status = IAU::Cal2jd((int)$year, (int)$month, (int)$day, $djm0, $djm);
        $this->checkDate($status);  // Check date is valid

        $this->jd = $djm0 + $djm;  // Only set JD, keep day frac to save time
        return $this;
    }

    /**
     * Sets the time of this instance
     *
     * @param  int   $hour Hour
     * @param  int   $min  Minute
     * @param  float $sec  Second
     *
     * @return static
     */
    public function setTime($hour, $min, $sec)
    {
        $status = IAU::Tf2d('+', (int)$hour, (int)$min, (float)$sec, $days);
        $this->checkTime($status);  // Check time is valid

        $this->dayFrac = $days;  // Only set the day fraction
        return $this;
    }

    /**
     * Sets the date and time of this instance
     *
     * @param  int   $year  Year
     * @param  int   $month Month
     * @param  int   $day   Day
     * @param  int   $hour  Hour
     * @param  int   $min   Minute
     * @param  float $sec   Second
     *
     * @return static
     */
    public function setDateTime($year, $month, $day, $hour, $min, $sec)
    {
        return $this->setDate($year, $month, $day)->setTime($hour, $min, $sec);
    }

    /**
     * Sets the time zone of this instance
     *
     * @param  TimeZone|string $timezone Either a TimeZone instance or string
     *
     * @return static
     *
     * @throws InvalidArgumentException Occurs if the timezone is an invalid
     *                                  type
     */
    public function setTimezone($timezone)
    {
        // Check type, and parse string if present
        if (is_string($timezone)) {
            $timezone = TimeZone::parse($timezone);
        } else {
            if ($timezone instanceof TimeZone == false) {
                throw new \InvalidArgumentException();
            }
        }

        // Convert back to UTC
        //$this->toUTC();
        // Compute new UTC offset
        $jd       = $this->toJD();
        $tzOffset = $timezone->offset($jd) - $this->timezone->offset($jd);

        $this->add(Time::hours($tzOffset));

        // Set the timezone
        $this->timezone  = $timezone;
        $this->timezone0 = $timezone;

        return $this;
    }

    /**
     * Returns a Julian date representation of this instance.
     *
     * @param  int $scale Optional additional decimal precision
     *
     * @return float|string
     */
    public function toJD($scale = null)
    {
        if ($scale) {
            return bcadd((string)$this->jd, (string)$this->dayFrac, $scale);
        } else {
            return $this->jd + $this->dayFrac;
        }
    }

    /**
     * Returns a Modified Julian date representation of this instance.
     *
     * @param  int $scale Optional additional decimal precision
     *
     * @return float|string
     */
    public function toMJD($scale = null)
    {
        $mjd = static::MJD;

        if ($scale) {
            return bcsub(bcadd($this->jd, $this->dayFrac, $scale), $mjd,
              $scale);
        } else {
            return $this->jd + $this->dayFrac - $mjd;
        }
    }

    /**
     * Adds a Time interval to this instance
     *
     * @param  Time $t
     *
     * @return static
     */
    public function add(Time $t)
    {
        // Interval to add as days
        $td = $t->days;

        // Days (jda) and day fraction (dfa) to add
        $jda = intval($td);
        $dfa = $this->dayFrac + $td - $jda;

        // Handle the event that the day fraction becomes negative
        if ($dfa < 0) {
            $dfa += 1;
            $jda -= 1;
        }

        // Additional day to add from above day frac in excess of 1 day
        $jda1 = intval($dfa);

        // Since additional day has been added, modulate day frac to range 0-1
        $dfa = fmod($dfa, 1);

        // Add the intervals
        $this->jd      = $this->jd + $jda + $jda1;
        $this->dayFrac = $dfa;

        return $this;
    }

    /**
     * Subtracts a Time interval from this instance
     *
     * @param  Time $t
     *
     * @return static
     */
    public function sub(Time $t)
    {
        return $this->add(Time::sec($t->sec * -1));
    }

    /**
     * Converts this instance to the Coordinated Universal Time scale (UTC), or
     * if a time zone was set then that timezone
     *
     * @return static
     * @throws Exception Occurs if UTC cannot be computed
     */
    public function toUTC()
    {
        if ($this->timescale == TimeScale::UTC()) {
            // Remove the timezone and set to UTC
            $offset = $this->timezone->offset($this->toJD());
            $this->sub(Time::hours($offset));
            $this->timezone = $this->timezone0 ? $this->timezone0 : TimeZone::UTC();

            return $this;
        }

        // TAI -> UTC
        if ($this->timescale == TimeScale::TAI()) {
            $tai1 = $this->jd;
            $tai2 = $this->dayFrac;
            IAU::Taiutc($tai1, $tai2, $utc1, $utc2);

            $this->jd        = $utc1;
            $this->dayFrac   = $utc2;
            $this->timescale = TimeScale::UTC();

            return $this;
        }

        // TT -> UTC
        if ($this->timescale == TimeScale::TT()) {
            $tt1 = $this->jd;
            $tt2 = $this->dayFrac;
            IAU::Tttai($tt1, $tt2, $tai1, $tai2);
            IAU::Taiutc($tai1, $tai2, $utc1, $utc2);

            $this->jd        = $utc1;
            $this->dayFrac   = $utc2;
            $this->timescale = TimeScale::UTC();

            return $this;
        }

        // UT1 -> UTC
        if ($this->timescale == TimeScale::UT1()) {
            $ut11 = $this->jd;
            $ut12 = $this->dayFrac;
            $dut1 = IERS::jd($ut11 + $ut12)->dut1();
            IAU::Ut1utc($ut11, $ut12, $dut1, $utc1, $utc2);

            $this->jd        = $utc1;
            $this->dayFrac   = $utc2;
            $this->timescale = TimeScale::UTC();

            return $this;
        }

        // TDB -> UTC
        if ($this->timescale == TimeScale::TDB()) {
            $tt1  = $this->jd;
            $tt2  = $this->dayFrac;
            $ut   = $this->dayFrac;
            $dtr  = IAU::Dtdb($tt1, $tt2, $ut, 0, 0, 0);
            $tdb1 = $this->jd;
            $tdb2 = $this->dayFrac;

            IAU::Tdbtt($tdb1, $tdb2, $dtr, $tt1, $tt2);
            IAU::Tttai($tt1, $tt2, $tai1, $tai2);
            IAU::Taiutc($tai1, $tai2, $utc1, $utc2);

            $this->jd        = $utc1;
            $this->dayFrac   = $utc2;
            $this->timescale = TimeScale::UTC();

            return $this;
        }

        throw new Exception('Error converting to UTC');
    }

    /**
     * Converts this instance to International Atomic Time (TAI)
     *
     * @return static
     */
    public function toTAI()
    {
        if ($this->timescale == TimeScale::TAI()) {
            return $this;
        }

        $this->toUTC();

        $utc1 = $this->jd;
        $utc2 = $this->dayFrac;
        IAU::Utctai($utc1, $utc2, $tai1, $tai2);

        $this->jd        = $tai1;
        $this->dayFrac   = $tai2;
        $this->timescale = TimeScale::TAI();

        return $this;
    }

    /**
     * Converts this instance to Terrestrial Dynamic Time (TT)
     *
     * @return static
     */
    public function toTT()
    {
        if ($this->timescale == TimeScale::TT()) {
            return $this;
        }

        $this->toTAI();

        $tai1 = $this->jd;
        $tai2 = $this->dayFrac;
        IAU::Taitt($tai1, $tai2, $tt1, $tt2);

        $this->jd        = $tt1;
        $this->dayFrac   = $tt2;
        $this->timescale = TimeScale::TT();

        return $this;
    }

    /**
     * Converts this instance to Universal Time (UT1)
     *
     * @return static
     */
    public function toUT1()
    {
        if ($this->timescale == TimeScale::UT1()) {
            return $this;
        }

        $this->toUTC();

        $utc1 = $this->jd;
        $utc2 = $this->dayFrac;
        $dut1 = IERS::jd($utc1 + $utc2)->dut1();
        IAU::Utcut1($utc1, $utc2, $dut1, $ut11, $ut12);

        $this->jd        = $ut11;
        $this->dayFrac   = $ut12;
        $this->timescale = TimeScale::UT1();

        return $this;
    }

    /**
     * Converts this instance to Barycentric Dynamic Time (TDB)
     *
     * @return static
     */
    public function toTDB()
    {
        if ($this->timescale == TimeScale::TDB()) {
            return $this;
        }

        $this->toTT();
        $tt1 = $this->jd;
        $tt2 = $this->dayFrac;

        $this->toUT1();
        $ut = $this->dayFrac;

        $dtr = IAU::Dtdb($tt1, $tt2, $ut, 0, 0, 0);
        IAU::Tttdb($tt1, $tt2, $dtr, $tdb1, $tdb2);

        $this->jd        = $tdb1;
        $this->dayFrac   = $tdb2;
        $this->timescale = TimeScale::TDB();

        return $this;
    }

    /**
     * Converts this instance to an Epoch
     *
     * @return Epoch
     */
    public function toEpoch()
    {
        return Epoch::dt($this->copy());
    }

    /**
     * Finds the month name of this instance
     *
     * @param  bool $full True for full name, false for abbreviation
     *
     * @return string
     */
    public function monthName($full = false)
    {
        $months = [
          [1, 'Jan', 'January'],
          [2, 'Feb', 'February'],
          [3, 'Mar', 'March'],
          [4, 'Apr', 'April'],
          [5, 'May', 'May'],
          [6, 'Jun', 'June'],
          [7, 'Jul', 'July'],
          [8, 'Aug', 'August'],
          [9, 'Sep', 'September'],
          [10, 'Oct', 'October'],
          [11, 'Nov', 'November'],
          [12, 'Dec', 'December'],
        ];

        return $months[$this->month - 1][$full ? 2 : 1];
    }

    /**
     * Returns if this instance is a leap year
     *
     * @return bool
     */
    public function isLeapYear()
    {
        return ($this->year % 4 == 0 && $this->year % 100 != 0) ||
        $this->year % 400 == 0;
    }

    /**
     * Returns the week day name of this instance
     *
     * @param  bool $full True for full name, false for abbreviation
     *
     * @return string
     */
    public function dayName($full = true)
    {
        $days = [
          [0, 'Sun', 'Sunday'],
          [1, 'Mon', 'Monday'],
          [2, 'Tue', 'Tuesday'],
          [3, 'Wed', 'Wednesday'],
          [4, 'Thu', 'Thursday'],
          [5, 'Fri', 'Friday'],
          [6, 'Sat', 'Saturday'],
        ];

        return $days[$this->weekDayNum()][$full ? 2 : 1];
    }

    /**
     * Finds the difference between this AstroDate and another
     *
     * @param  AstroDate $b
     *
     * @return static
     */
    public function diff(AstroDate $b)
    {
        $prec = 12;
        $jd1  = $this->toJD($prec);
        $jd2  = $b->toJD($prec);
        $days = bcsub($jd1, $jd2, $prec);

        return Time::days(-1 * $days);
    }

    /**
     * Returns the day of the year number, 0-356 or 0-366 for leap year
     *
     * @return int
     */
    public function dayOfYear()
    {
        $k = $this->isLeapYear() ? 1 : 2;
        $n = intval(275 * (int)$this->month / 9) -
          $k * intval(((int)$this->month + 9) / 12) +
          (int)$this->day - 30;

        return (int)$n;
    }

    /**
     * Finds the sidereal time of this intsance
     *
     * @param type  $mode Type of sidereal time... (a = apparent, m = mean)
     * @param Angle $lon  If a longitude is supplied, finds local sidereal time,
     *                    otherwise returns sidereal time at Greenwich
     */
    public function sidereal($mode = 'a', Angle $lon = null)
    {
        // Get UT1 time
        $ut  = $this->copy()->toUT1();
        $uta = $ut->jd;
        $utb = $ut->dayFrac;
        $ut  = null;

        // Get TT time
        $tt  = $this->copy()->toTT();
        $tta = $tt->jd;
        $ttb = $tt->dayFrac;
        $tt  = null;

        // Compute either GMST or GAST
        $st;
        if ($mode == 'a') {
            $strad = IAU::Gst06a($uta, $utb, $tta, $ttb);
        } else {
            $strad = IAU::Gmst06($uta, $utb, $tta, $ttb);
        }

        // Add longitude if relevant
        if ($lon) {
            $st = Angle::rad($strad)->add($lon)->norm()->toTime();
        } else {
            $st = Angle::rad($strad)->toTime();
        }

        // Return as hours
        return $st;
    }

    /**
     * Returns the time elapsed since midnight
     *
     * @return Time
     */
    public function sinceMidnight()
    {
        return Time::days($this->dayFrac);
    }

    /**
     * Returns the time left until since midnight
     *
     * @return Time
     */
    public function untilMidnight()
    {
        return Time::days(1 - $this->dayFrac);
    }

    // // // Protected

    /**
     * Throws an exception for a provided IAU date error code
     *
     * @param  int $status IAU date error
     *
     * @throws Exception
     */
    protected function checkDate($status)
    {
        switch ($status) {
            case 3:  // Ignore dubious year
                //throw new Exception('time is after end of day and dubious year');

            case 2:
                throw new Exception('time is after end of day');

            //case 1:   // Ignore dubious year
            //throw new Exception('dubious year');

            case -1:
                throw new Exception('bad year');

            case -2:
                throw new Exception('bad month');

            case -3:
                throw new Exception('bad day');

            case -4:
                throw new Exception('bad hour');

            case -5:
                throw new Exception('bad minute');

            case -6:
                throw new Exception('bad second');

            default:
                return;
        }
    }

    /**
     * Throws an exception for a provided IAU time error code
     *
     * @param  int $status IAU time error
     *
     * @throws Exception
     */
    protected function checkTime($status)
    {
        switch ($status) {
            case 1:
                throw new Exception('hour outside range 0-23');

            case 2:
                throw new Exception('min outside range 0-59');

            case 3:
                throw new Exception('sec outside range 0-59.999...');

            default:
                return;
        }
    }

    /**
     * Gets a component of this date
     *
     * @param  string $e Component name, e.g. year, month, etc...
     *
     * @return int|float
     */
    protected function getComponent($e)
    {
        // JD -> Date
        $ihmsf = [];
        IAU::D2dtf($this->timescale, $this->prec - 2, $this->jd, $this->dayFrac,
          $iy, $im, $id, $ihmsf);

        switch ($e) {
            case 'year':
                return $iy;

            case 'month':
                return $im;

            case 'day':
                return $id;

            case 'hour':
                return $ihmsf[0];

            case 'min':
                return $ihmsf[1];

            case 'sec':
                return $ihmsf[2];

            case 'micro':
                return $ihmsf[3];
        }
    }

    /**
     * Calculates a number representing the day of the week for this instance
     *
     * @return int
     */
    protected function weekDayNum()
    {
        return ($this->jd + 1.5) % 7;
    }

    // // // Static

    /**
     * Finds the number for a month represented as a string
     *
     * @param  string $month
     *
     * @return int
     */
    protected static function monthNum($month)
    {
        switch (strtolower(substr($month, 0, 3))) {
            case 'jan':
                return 1;
            case 'feb':
                return 2;
            case 'mar':
                return 3;
            case 'apr':
                return 4;
            case 'may':
                return 5;
            case 'jun':
                return 6;
            case 'jul':
                return 7;
            case 'aug':
                return 8;
            case 'sep':
                return 9;
            case 'oct':
                return 10;
            case 'nov':
                return 11;
            case 'dec':
                return 12;

            default:
                return null;
        }
    }

    /**
     * Finds the ordinal suffix for a number, e.g. 1st, 2nd, 3rd, etc...
     *
     * @param  int $number
     *
     * @return string
     */
    protected static function ordinal($number)
    {
        $sn = (string)$number;

        if ($number < 11 || $number > 13) {
            if (substr($sn, strlen($sn) - 1, 1) == 1) {
                return 'st';
            }
            if (substr($sn, strlen($sn) - 1, 1) == 2) {
                return 'nd';
            }
            if (substr($sn, strlen($sn) - 1, 1) == 3) {
                return 'rd';
            } else {
                return 'th';
            }
        } else {
            return 'th';
        }
    }

    // // // Overrides

    /**
     * Represents this instance as a string with the current format
     *
     * @return string
     */
    public function __toString()
    {
        return $this->format($this->format);
    }

}


<?php

namespace Ekyna\Component\Commerce\Bridge\Payum\OutstandingBalance\Action;

use Ekyna\Component\Commerce\Bridge\Payum\OutstandingBalance\Constants;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\GetStatusInterface;

/**
 * Class StatusAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\OutstandingBalance\Action
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StatusAction implements ActionInterface
{
    /**
     * {@inheritDoc}
     */
    public function execute($request)
    {
        /** @var GetStatusInterface $request  */
        RequestNotSupportedException::assertSupports($this, $request);

        $model = ArrayObject::ensureArrayObject($request->getModel());

        if (!isset($model[Constants::FIELD_STATUS])) {
            $request->markNew();

            return;
        }

        if (Constants::STATUS_AUTHORIZED == $model[Constants::FIELD_STATUS]) {
            $request->markAuthorized();

            return;
        }

        if (Constants::STATUS_CAPTURED == $model[Constants::FIELD_STATUS]) {
            $request->markCaptured();

            return;
        }

        if (Constants::STATUS_FAILED == $model[Constants::FIELD_STATUS]) {
            $request->markFailed();

            return;
        }

        if (Constants::STATUS_EXPIRED == $model[Constants::FIELD_STATUS]) {
            $request->markExpired();

            return;
        }

        if (Constants::STATUS_CANCELED == $model[Constants::FIELD_STATUS]) {
            $request->markCanceled();

            return;
        }

        $request->markUnknown();
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return
            $request instanceof GetStatusInterface &&
            $request->getModel() instanceof \ArrayAccess;
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Payum\Payzen\Action;

use Ekyna\Component\Commerce\Bridge\Payum\Request\FraudLevel;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;

/**
 * Class FraudAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\Payzen\Action
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class FraudLevelAction implements ActionInterface
{
    /**
     * {@inheritDoc}
     *
     * @param FraudLevel $request
     */
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);

        /** @var PaymentInterface $payment */
        $payment = $request->getModel();

        if ($payment->getState() !== PaymentStates::STATE_FAILED) {
            return;
        }

        $model = ArrayObject::ensureArrayObject($payment->getDetails());

        if (false == $result = $model['vads_auth_result']) {
            return;
        }

        switch ($result) {
            case '33': // Date de validité de la carte dépassée
            case '54': // Date de validité de la carte dépassée
                $request->setLevel(1);

                return;

            case '03': // Accepteur invalide
            case '04': // Conserver la carte
            case '05': // Ne pas honorer
            case '07': // Conserver la carte, conditions spéciales
            case '12': // Transaction invalide
            case '13': // Montant invalide
                $request->setLevel(3);

                return;

            case '34': // Suspicion de fraude
            case '59': // Suspicion de fraude
                $request->setLevel(4);

                return;

            case '14': // Numéro de porteur invalide
            case '15': // Emetteur de carte inconnu
            case '31': // Identifiant de l’organisme acquéreur inconnu
            case '41': // Carte perdue
            case '43': // Carte volée
            case '56': // Carte absente du fichier
            case '57': // Transaction non permise à ce porteur
            case '63': // Règles de sécurité non respectées
            case '76': // Porteur déjà en opposition, ancien enregistrement conservé
                $request->setLevel(5);

                return;
        }

        if ($model['vads_ctx_mode'] === 'TEST' && $result === '51') { // Provision insuffisante ou crédit dépassé
            $request->setLevel(3);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return $request instanceof FraudLevel
            && $request->getModel() instanceof PaymentInterface;
    }
}


<?php

declare(strict_types=1);

/**
 * Immutable.
 *
 * LICENSE
 *
 * This source file is subject to the MIT license and the version 3 of the GPL3
 * license that are bundled with this package in the folder licences
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to richarddeloge@gmail.com so we can send you a copy immediately.
 *
 *
 * @copyright   Copyright (c) 2009-2016 Richard Déloge (richarddeloge@gmail.com)
 *
 * @link        http://teknoo.software/imuutable Project website
 *
 * @license     http://teknoo.software/license/mit         MIT License
 * @author      Richard Déloge <richarddeloge@gmail.com>
 */

namespace Teknoo\Immutable;

use Teknoo\Immutable\Exception\ImmutableException;

/**
 * Trait ImmutableTrait
 * Default implementation of ImmutableInterface
 * Implement also a standard constructor to avoid multiple call of this method.
 *
 * @copyright   Copyright (c) 2009-2016 Richard Déloge (richarddeloge@gmail.com)
 *
 * @link        http://teknoo.software/imuutable Project website
 *
 * @license     http://teknoo.software/license/mit         MIT License
 * @author      Richard Déloge <richarddeloge@gmail.com>
 */
trait ImmutableTrait
{
    /**
     * @var bool
     */
    private $isConstructed = false;

    /**
     * Method to call in the constructor to avoid multiple call of '__construct'.
     *
     * @return self|ImmutableInterface
     */
    protected function uniqueConstructorCheck(): ImmutableInterface
    {
        if (true === $this->isConstructed) {
            throw new ImmutableException('This object is immutable');
        }

        $this->isConstructed = true;

        return $this;
    }

    /**
     * ImmutableTrait default constructor.
     */
    public function __construct()
    {
        $this->uniqueConstructorCheck();
    }

    /**
     * {@inheritdoc}
     */
    public function __set(string $name, $value)
    {
        throw new ImmutableException('This object is immutable');
    }

    /**
     * {@inheritdoc}
     */
    public function __unset(string $name)
    {
        throw new ImmutableException('This object is immutable');
    }
}


<?php

namespace PeskyCMF\Db\Traits;

use PeskyCMF\Db\CmfDbTable;
use PeskyORM\DbObject;
use PeskyORM\Exception\DbModelException;

trait TaggedCacheForDbSelects {

    use CacheForDbSelects;

    /**
     * Detect if caching is possible
     * @return bool
     */
    protected function cachingIsPossible() {
        if (static::$_cachingIsPossible === null) {
            /** @var \AlternativeLaravelCache\Core\AlternativeCacheStore $cache */
            $storeClass = '\AlternativeLaravelCache\Core\AlternativeCacheStore';
            $poolInterface = '\Cache\Taggable\TaggablePoolInterface';
            $cache = app('cache.store')->getStore();
            static::$_cachingIsPossible = (
                $cache instanceof $storeClass
                && $cache->getWrappedConnection() instanceof $poolInterface
            );
        }
        return static::$_cachingIsPossible;
    }

    /**
     * @param bool|true $cleanRelatedModelsCache - true: clean cache for all related models
     * @throws \BadMethodCallException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbUtilsException
     * @throws \InvalidArgumentException
     */
    public function cleanModelCache($cleanRelatedModelsCache = null) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        \Cache::tags($this->getModelCachePrefix())->flush();
        $this->cleanRelatedModelsCache($cleanRelatedModelsCache);
    }

    /**
     * Clean cache for all related models
     * @param bool|null|array|string $cleanRelatedModelsCache -
     *      - array: list of relations to clean
     *      - string: single relation to clean
     *      - bool: true = clean all relations provided by $this->getRelationsToCleanByDefault(); false - don't clean relations
     *      - null: if ($this->canCleanRelationsCache() === true) then clean $this->getRelationsToCleanByDefault()
     *
     * @throws \BadMethodCallException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbUtilsException
     * @throws \InvalidArgumentException
     * @throws \PeskyORM\Exception\DbTableConfigException
     */
    public function cleanRelatedModelsCache($cleanRelatedModelsCache = true) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        if ($cleanRelatedModelsCache === null) {
            $cleanRelatedModelsCache = $this->canCleanRelationsCache();
        }
        if ($cleanRelatedModelsCache) {
            if (is_array($cleanRelatedModelsCache)) {
                $relationsToClean = $cleanRelatedModelsCache;
            } else if (is_string($cleanRelatedModelsCache)) {
                $relationsToClean = [$cleanRelatedModelsCache];
            } else {
                $relationsToClean = $this->getDefaultRelationsForCacheCleaner();
            }
            $tags = [];
            foreach ($relationsToClean as $relationKey) {
                if (!$this->hasTableRelation($relationKey)) {
                    throw new \InvalidArgumentException("Model has no relation named $relationKey");
                }
                /** @var CmfDbTable|TaggedCacheForDbSelects $model */
                $model = $this->getRelatedModel($relationKey);
                $tags[] = $model->getModelCachePrefix();
            }
            if (!empty($tags)) {
                \Cache::tags($tags)->flush();
            }
        }
    }

    /**
     * @return array - relations names
     * @throws \PeskyORM\Exception\DbModelException
     */
    protected function getDefaultRelationsForCacheCleaner() {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        return array_keys($this->getTableRealtaions());
    }

    /**
     * @param bool $cleanRelatedModelsCache
     * @throws \BadMethodCallException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbUtilsException
     * @throws \InvalidArgumentException
     */
    public function cleanSelectManyCache($cleanRelatedModelsCache = null) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        \Cache::tags($this->getSelectManyCacheTag())->flush();
        $this->cleanRelatedModelsCache($cleanRelatedModelsCache);
    }

    /**
     * @param bool $cleanRelatedModelsCache
     * @throws \BadMethodCallException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbUtilsException
     * @throws \InvalidArgumentException
     */
    public function cleanSelectOneCache($cleanRelatedModelsCache = null) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        \Cache::tags($this->getSelectOneCacheTag())->flush();
        $this->cleanRelatedModelsCache($cleanRelatedModelsCache);
    }

    /**
     * @param int|string|array|DbObject $record
     * @param bool $cleanRelatedModelsCache
     * @throws DbModelException
     * @throws \BadMethodCallException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \PeskyORM\Exception\DbObjectException
     * @throws \PeskyORM\Exception\DbUtilsException
     * @throws \InvalidArgumentException
     */
    public function cleanRecordCache($record, $cleanRelatedModelsCache = null) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        if (!($record instanceof DbObject) || $record->exists()) {
            \Cache::tags($this->getRecordCacheTag($record))->flush();
        }
        $this->cleanRelatedModelsCache($cleanRelatedModelsCache);
    }

    /**
     * @return string
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     */
    public function getSelectManyCacheTag() {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        return $this->getModelCachePrefix() . 'many';
    }

    /**
     * @return string
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     */
    public function getSelectOneCacheTag() {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        return $this->getModelCachePrefix() . 'one';
    }

    /**
     * @param mixed $record
     * @return string
     * @throws \PeskyORM\Exception\DbObjectException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws DbModelException
     */
    public function getRecordCacheTag($record) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        if ($record instanceof DbObject) {
            $id = $record->exists() ? $record->_getPkValue() : null;
        } else if (!is_array($record)) {
            $id = $record;
        } else if (!empty($record[$this->getPkColumnName()])) {
            $id = $record[$this->getPkColumnName()];
        } else if (!empty($record[$this->getAlias()]) && !empty($record[$this->getAlias()][$this->getPkColumnName()])) {
            $id = $record[$this->getAlias()][$this->getPkColumnName()];
        }
        if (empty($id)) {
            throw new DbModelException($this, 'Data passed to getRecordCacheTag() has no value for primary key');
        }
        return $this->getModelCachePrefix() . 'id=' . $id;
    }

    /**
     * Get data from cache or put data from $callback to cache
     * @param bool $affectsSingleRecord
     * @param array $cacheSettings - [
     *      'key' => 'string, cache key',
     *      'timeout' => 'int (minutes or unix timestamp), \DateTime, null (infinite)',
     *      'tags' => ['custom', 'cache', 'tags'],
     *      'recache' => 'bool, ignore cached data and replace it with fresh data'
     * ]
     * @param \Closure $callback
     *
     * @return array
     * @throws \PeskyORM\Exception\DbObjectException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     * @throws \BadMethodCallException
     */
    protected function _getCachedData($affectsSingleRecord, array $cacheSettings, \Closure $callback) {
        /** @var CmfDbTable|TaggedCacheForDbSelects $this */
        $data = empty($cacheSettings['recache']) ? \Cache::get($cacheSettings['key'], '{!404!}') : '{!404!}';
        if ($data === '{!404!}') {
            $data = $callback();
            if ($data instanceof DbObject) {
                $data = $data->exists() ? $data->toPublicArray() : [];
            }
            $tags = $cacheSettings['tags'];
            $tags[] = $this->getModelCachePrefix();
            if ($affectsSingleRecord) {
                $tags[] = $this->getSelectOneCacheTag();
                if (!empty($data) && is_array($data) && !empty($data[$this->getPkColumnName()])) {
                    // create tag only for record with primary key value
                    $tags[] = $this->getRecordCacheTag($data);
                }
            } else {
                $tags[] = $this->getSelectManyCacheTag();
            }
            $cacher = \Cache::tags($tags);
            if (empty($cacheSettings['timeout'])) {
                $cacher->forever($cacheSettings['key'], $data);
            } else {
                $cacher->put($cacheSettings['key'], $data, $cacheSettings['timeout']);
            }
        }
        return $data;
    }

    /**
     * @param bool $affectsSingleRecord
     * @param null|string|array $columns
     * @param null|array $conditionsAndOptions
     * @return string
     * @throws \InvalidArgumentException
     * @throws \PeskyORM\Exception\DbModelException
     * @throws \PeskyORM\Exception\DbConnectionConfigException
     */
    public function buildDefaultCacheKey($affectsSingleRecord, $columns, $conditionsAndOptions) {
        $prefix = $affectsSingleRecord ? $this->getSelectOneCacheTag() : $this->getSelectManyCacheTag();
        return $prefix . '.' . static::buildCacheKey($columns, $conditionsAndOptions);
    }

}


<?php

namespace Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository;

use Ekyna\Component\Commerce\Common\Model\CurrencyInterface;
use Ekyna\Component\Commerce\Payment\Entity\PaymentMessage;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Repository\PaymentMethodRepositoryInterface;
use Ekyna\Component\Resource\Doctrine\ORM\TranslatableResourceRepository;

/**
 * Class PaymentMethodRepository
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class PaymentMethodRepository extends TranslatableResourceRepository implements PaymentMethodRepositoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function createNew()
    {
        /** @var \Ekyna\Component\Commerce\Payment\Model\PaymentMethodInterface $method */
        $method = parent::createNew();

        foreach (PaymentStates::getNotifiableStates() as $state) {
            $message = new PaymentMessage();
            $method->addMessage($message->setState($state));
        }

        return $method;
    }

    /**
     * @inheritDoc
     */
    public function findAvailable(CurrencyInterface $currency = null)
    {
        $qb = $this->createQueryBuilder('m');
        $ex = $qb->expr();

        $parameters = [
            'enabled'   => true,
            'available' => true,
        ];

        $qb
            ->andWhere($ex->eq('m.enabled', ':enabled'))
            ->andWhere($ex->eq('m.available', ':available'))
            ->orderBy('m.position', 'ASC');

        if ($currency) {
            $parameters['currency'] = $currency;
            $qb->andWhere($ex->orX(
                $qb->expr()->isMemberOf(':currency', 'm.currencies'),
                'm.currencies IS EMPTY'
            ));
        }

        return $qb
            ->getQuery()
            ->setParameters($parameters)
            ->getResult();
    }

    /**
     * @inheritDoc
     */
    public function findEnabled(CurrencyInterface $currency = null)
    {
        $qb = $this->createQueryBuilder('m');
        $ex = $qb->expr();

        $parameters = [
            'enabled'   => true,
        ];

        $qb
            ->andWhere($ex->eq('m.enabled', ':enabled'))
            ->orderBy('m.position', 'ASC');

        if ($currency) {
            $parameters['currency'] = $currency;
            $qb->andWhere($ex->orX(
                $qb->expr()->isMemberOf(':currency', 'm.currencies'),
                'm.currencies IS EMPTY'
            ));
        }

        return $qb
            ->getQuery()
            ->setParameters($parameters)
            ->getResult();
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Listener;

use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Ekyna\Component\Commerce\Common\Model\IdentityInterface;
use Ekyna\Component\Commerce\Payment\Model as Payment;
use Ekyna\Component\Commerce\Pricing as Pricing;
use Ekyna\Component\Commerce\Stock\Entity\AbstractStockUnit;
use Ekyna\Component\Commerce\Stock\Model as Stock;
use Ekyna\Component\Commerce\Subject\Entity\SubjectIdentity;
use Ekyna\Component\Commerce\Subject\Model\SubjectRelativeInterface;
use Ekyna\Component\Resource\Doctrine\ORM\Mapping\DiscriminatorMapper;
use Ekyna\Component\Resource\Doctrine\ORM\Mapping\EmbeddableMapper;

/**
 * Class LoadMetadataListener
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Listener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class LoadMetadataListener implements EventSubscriber
{
    /**
     * @var DiscriminatorMapper
     */
    private $stockUnitMapper;

    /**
     * @var EmbeddableMapper
     */
    private $subjectIdentityMapper;

    /**
     * @var array
     */
    private $paymentTermSubjectClassCache = [];

    /**
     * @var array
     */
    private $identityClassCache = [];

    /**
     * @var array
     */
    private $vatNumberSubjectClassCache = [];

    /**
     * @var array
     */
    private $taxableClassCache = [];

    /**
     * @var array
     */
    private $relativeClassCache = [];

    /**
     * @var array
     */
    private $stockClassCache = [];

    /**
     * @inheritdoc
     */
    public function getSubscribedEvents()
    {
        return [Events::loadClassMetadata];
    }

    /**
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        // Skip mapped super classes
        if ($metadata->isMappedSuperclass) {
            return;
        }

        $class = $metadata->getName();
        // Skip abstract classes.
        if ((new \ReflectionClass($class))->isAbstract()) {
            return;
        }

        if (is_subclass_of($class, IdentityInterface::class)) {
            $this->configureIdentityMapping($eventArgs);
        }

        if (is_subclass_of($class, Pricing\Model\TaxableInterface::class)) {
            $this->configureTaxableMapping($eventArgs);
        }

        if (is_subclass_of($class, Pricing\Model\VatNumberSubjectInterface::class)) {
            $this->configureVatNumberSubjectMapping($eventArgs);
        }

        if (is_subclass_of($class, Payment\PaymentTermSubjectInterface::class)) {
            $this->configurePaymentTermSubjectMapping($eventArgs);
        }

        if (is_subclass_of($class, Stock\StockSubjectInterface::class)) {
            $this->configureStockSubjectMapping($eventArgs);
        }

        if (is_subclass_of($class, SubjectRelativeInterface::class)) {
            $this->configureSubjectRelativeMapping($eventArgs);
        }

        if (is_subclass_of($class, Stock\StockUnitInterface::class)) {
            $this->configureStockUnitDiscriminatorMap($eventArgs);
        }
    }

    /**
     * Configures the taxable mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureTaxableMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, Pricing\Model\TaxableInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->taxableClassCache)) {
            return;
        }

        if (!$metadata->hasAssociation('taxGroup')) {
            $metadata->mapManyToOne([
                'fieldName'    => 'taxGroup',
                'targetEntity' => Pricing\Entity\TaxGroup::class,
                'joinColumns'  => [
                    [
                        'name'                 => 'tax_group_id',
                        'referencedColumnName' => 'id',
                        'onDelete'             => 'RESTRICT',
                        'nullable'             => true,
                    ],
                ],
            ]);
        }

        // Cache class
        $this->taxableClassCache[] = $class;
    }

    /**
     * Configures the identity mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureIdentityMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, IdentityInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->identityClassCache)) {
            return;
        }

        // Add mappings
        $this->addMappings($metadata, $this->getIdentityMappings());

        // Cache class
        $this->identityClassCache[] = $class;
    }

    /**
     * Configures the vat number subject mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureVatNumberSubjectMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, Pricing\Model\VatNumberSubjectInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->vatNumberSubjectClassCache)) {
            return;
        }

        // Add mappings
        $this->addMappings($metadata, $this->getVatNumberSubjectMappings());

        // Cache class
        $this->vatNumberSubjectClassCache[] = $class;
    }

    /**
     * Configures the payment term subject mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configurePaymentTermSubjectMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, Payment\PaymentTermSubjectInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->paymentTermSubjectClassCache)) {
            return;
        }

        if (!$metadata->hasAssociation('paymentTerm')) {
            $metadata->mapManyToOne([
                'fieldName'    => 'paymentTerm',
                'targetEntity' => Payment\PaymentTermInterface::class,
                'joinColumns'  => [
                    [
                        'name'                 => 'payment_term_id',
                        'referencedColumnName' => 'id',
                        'onDelete'             => 'RESTRICT',
                        'nullable'             => true,
                    ],
                ],
            ]);
        }

        // Cache class
        $this->paymentTermSubjectClassCache[] = $class;
    }

    /**
     * Configures the subject relative mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureSubjectRelativeMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, SubjectRelativeInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->relativeClassCache)) {
            return;
        }

        // Map embedded
        $this
            ->getSubjectIdentityMapper($eventArgs->getEntityManager())
            ->processClassMetadata($metadata, 'subjectIdentity', 'subject_');

        // Cache class
        $this->relativeClassCache[] = $class;
    }

    /**
     * Configures the stock subject mapping.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureStockSubjectMapping(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();
        $class = $metadata->getName();

        // Check class
        if (!is_subclass_of($class, Stock\StockSubjectInterface::class)) {
            return;
        }

        // Don't add twice
        if (in_array($class, $this->stockClassCache)) {
            return;
        }

        // Add mappings
        $this->addMappings($metadata, $this->getStockSubjectMappings());

        // Cache class
        $this->stockClassCache[] = $class;
    }

    /**
     * Configures the stock unit discriminator map.
     *
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    private function configureStockUnitDiscriminatorMap(LoadClassMetadataEventArgs $eventArgs)
    {
        /** @var ClassMetadata $metadata */
        $metadata = $eventArgs->getClassMetadata();

        if (!is_subclass_of($metadata->name, Stock\StockUnitInterface::class)) {
            return;
        }

        $this
            ->getStockUnitMapper($eventArgs->getEntityManager())
            ->processClassMetadata($metadata);
    }

    /**
     * Returns the stock unit mapper.
     *
     * @param EntityManagerInterface $em
     *
     * @return DiscriminatorMapper
     */
    private function getStockUnitMapper(EntityManagerInterface $em)
    {
        if (null === $this->stockUnitMapper) {
            $this->stockUnitMapper = new DiscriminatorMapper($em, AbstractStockUnit::class);
        }

        return $this->stockUnitMapper;
    }

    /**
     * Returns the subjectIdentityMapper.
     *
     * @param EntityManagerInterface $em
     *
     * @return EmbeddableMapper
     */
    private function getSubjectIdentityMapper(EntityManagerInterface $em)
    {
        if (null === $this->subjectIdentityMapper) {
            $this->subjectIdentityMapper = new EmbeddableMapper($em, SubjectIdentity::class);
        }

        return $this->subjectIdentityMapper;
    }

    /**
     * Adds the mappings to the metadata.
     *
     * @param ClassMetadata $metadata
     * @param array         $mappings
     */
    private function addMappings(ClassMetadata $metadata, array $mappings)
    {
        foreach ($mappings as $mapping) {
            if (!$metadata->hasField($mapping['fieldName'])) {
                $metadata->mapField($mapping);
            }
        }
    }

    /**
     * Returns the vat number subject mappings.
     *
     * @return array
     */
    private function getIdentityMappings()
    {
        return [
            [
                'fieldName'  => 'gender',
                'columnName' => 'gender',
                'type'       => 'string',
                'length'     => 8,
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'firstName',
                'columnName' => 'first_name',
                'type'       => 'string',
                'length'     => 64, // TODO length 32
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'lastName',
                'columnName' => 'last_name',
                'type'       => 'string',
                'length'     => 64, // TODO length 32
                'nullable'   => true,
            ],
        ];
    }

    /**
     * Returns the vat number subject mappings.
     *
     * @return array
     */
    private function getVatNumberSubjectMappings()
    {
        return [
            [
                'fieldName'  => 'vatNumber',
                'columnName' => 'vat_number',
                'type'       => 'string',
                'length'     => 32,
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'vatDetails',
                'columnName' => 'vat_details',
                'type'       => 'json_array',
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'vatValid',
                'columnName' => 'vat_valid',
                'type'       => 'boolean',
            ],
        ];
    }

    /**
     * Returns the stock subject mappings.
     *
     * @return array
     */
    private function getStockSubjectMappings()
    {
        return [
            [
                'fieldName'  => 'stockMode',
                'columnName' => 'stock_mode',
                'type'       => 'string',
                'length'     => 16,
                'nullable'   => false,
                'default'    => Stock\StockSubjectModes::MODE_AUTO,
            ],
            [
                'fieldName'  => 'stockState',
                'columnName' => 'stock_state',
                'type'       => 'string',
                'length'     => 16,
                'nullable'   => false,
                'default'    => Stock\StockSubjectStates::STATE_OUT_OF_STOCK,
            ],
            [
                'fieldName'  => 'stockFloor',
                'columnName' => 'stock_floor',
                'type'       => 'decimal',
                'precision'  => 10,
                'scale'      => 3,
                'nullable'   => true,
                'default'    => 0,
            ],
            [
                'fieldName'  => 'inStock',
                'columnName' => 'in_stock',
                'type'       => 'decimal',
                'precision'  => 10,
                'scale'      => 3,
                'nullable'   => false,
                'default'    => 0,
            ],
            [
                'fieldName'  => 'availableStock',
                'columnName' => 'available_stock',
                'type'       => 'decimal',
                'precision'  => 10,
                'scale'      => 3,
                'nullable'   => false,
                'default'    => 0,
            ],
            [
                'fieldName'  => 'virtualStock',
                'columnName' => 'virtual_stock',
                'type'       => 'decimal',
                'precision'  => 10,
                'scale'      => 3,
                'nullable'   => false,
                'default'    => 0,
            ],
            [
                'fieldName'  => 'replenishmentTime',
                'columnName' => 'replenishment_time',
                'type'       => 'smallint',
                'nullable'   => false,
                'default'    => 7,
            ],
            [
                'fieldName'  => 'estimatedDateOfArrival',
                'columnName' => 'estimated_date_of_arrival',
                'type'       => 'datetime',
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'geocode',
                'columnName' => 'geocode',
                'type'       => 'string',
                'length'     => 16,
                'nullable'   => true,
            ],
            [
                'fieldName'  => 'minimumOrderQuantity',
                'columnName' => 'minimum_order_quantity',
                'type'       => 'decimal',
                'precision'  => 10,
                'scale'      => 3,
                'nullable'   => false,
                'default'    => 1,
            ],
            [
                'fieldName'  => 'quoteOnly',
                'columnName' => 'quote_only',
                'type'       => 'boolean',
                'nullable'   => false,
                'default'    => false,
            ],
            [
                'fieldName'  => 'endOfLife',
                'columnName' => 'end_of_life',
                'type'       => 'boolean',
                'nullable'   => false,
                'default'    => false,
            ],
        ];
    }
}


<?php

declare(strict_types=1);

namespace FondBot\Drivers\Telegram;

use FondBot\Channels\Chat;
use FondBot\Channels\User;
use FondBot\Events\Unknown;
use FondBot\Channels\Driver;
use FondBot\Contracts\Event;
use Illuminate\Http\Request;
use React\EventLoop\Factory;
use FondBot\Contracts\Template;
use unreal4u\TelegramAPI\TgLog;
use FondBot\Templates\Attachment;
use React\EventLoop\LoopInterface;
use FondBot\Events\MessageReceived;
use unreal4u\TelegramAPI\Telegram\Types\Update;
use unreal4u\TelegramAPI\HttpClientRequestHandler;
use unreal4u\TelegramAPI\Telegram\Methods\SendAudio;
use unreal4u\TelegramAPI\Telegram\Methods\SendPhoto;
use unreal4u\TelegramAPI\Telegram\Methods\SendVideo;
use unreal4u\TelegramAPI\Telegram\Methods\SendMessage;
use unreal4u\TelegramAPI\Telegram\Methods\SendDocument;
use unreal4u\TelegramAPI\Telegram\Types\Custom\InputFile;
use unreal4u\TelegramAPI\Telegram\Methods\EditMessageReplyMarkup;

/**
 * @method TgLog getClient()
 */
class TelegramDriver extends Driver
{
    protected $token;

    /** @var TgLog */
    protected $client;

    /** @var LoopInterface */
    private $loop;

    /** @var Update|null */
    private $update;

    /**
     * Get gateway display name.
     *
     * This can be used for various system where human-friendly name is required.
     *
     * @return string
     */
    public function getName(): string
    {
        return 'Telegram';
    }

    /**
     * Get driver short name.
     *
     * This name is used as an alias for configuration.
     *
     * @return string
     */
    public function getShortName(): string
    {
        return 'telegram';
    }

    /**
     * Create API client.
     *
     * @return mixed
     */
    public function createClient(): TgLog
    {
        $this->loop = Factory::create();
        $handler = new HttpClientRequestHandler($this->loop);

        return new TgLog($this->token, $handler);
    }

    /**
     * Create event based on incoming request.
     *
     * @param Request $request
     *
     * @return Event
     */
    public function createEvent(Request $request): Event
    {
        if (empty($request->input())) {
            return new Unknown();
        }

        $update = new Update($request->input());
        $this->update = $update;

        if ($message = $update->message) {
            $chat = new Chat((string) $message->chat->id, $message->chat->title, $message->chat->type);
            $from = new User((string) $message->from->id, $message->from->first_name, $message->from->username);

            return new MessageReceived(
                $chat,
                $from,
                $message->text,
                $message->location,
                null,
                optional($update->callback_query)->data,
                $update
            );
        }

        if ($callbackQuery = $update->callback_query) {
            $message = $callbackQuery->message;

            $chat = new Chat((string) $message->chat->id, $message->chat->title, $message->chat->type);
            $from = new User((string) $message->chat->id, $message->from->first_name, $message->from->username);

            return new MessageReceived(
                $chat,
                $from,
                $message->text,
                $message->location,
                null,
                $callbackQuery->data,
                $update
            );
        }

        return new Unknown();
    }

    /**
     * Send message.
     *
     * @param Chat $chat
     * @param User $recipient
     * @param string $text
     * @param Template|null $template
     */
    public function sendMessage(Chat $chat, User $recipient, string $text, Template $template = null): void
    {
        $sendMessage = new SendMessage();

        if ($template !== null) {
            $sendMessage->reply_markup = $this->templateCompiler->compile($template);
        }

        $sendMessage->chat_id = $chat->getId();
        $sendMessage->text = $text;

        $this->client->performApiRequest($sendMessage);
        $this->loop->run();
    }

    /**
     * Send attachment.
     *
     * @param Chat $chat
     * @param User $recipient
     * @param Attachment $attachment
     */
    public function sendAttachment(Chat $chat, User $recipient, Attachment $attachment): void
    {
        $type = $attachment->getType();

        $request = null;

        switch ($type) {
            case Attachment::TYPE_FILE:
                $request = new SendDocument();
                $request->document = new InputFile($attachment->getPath());
                $request->caption = $attachment->getParameters()->get('caption');
                break;

            case Attachment::TYPE_IMAGE:
                $request = new SendPhoto();
                $request->caption = $attachment->getParameters()->get('caption');
                break;

            case Attachment::TYPE_AUDIO:
                $request = new SendAudio();
                $request->chat_id = $chat->getId();
                $request->caption = $attachment->getParameters()->get('caption');
                $request->duration = $attachment->getParameters()->get('duration');
                $request->performer = $attachment->getParameters()->get('performer');
                $request->title = $attachment->getParameters()->get('title');

                break;

            case Attachment::TYPE_VIDEO:
                $request = new SendVideo();
                $request->duration = $attachment->getParameters()->get('duration');
                $request->width = $attachment->getParameters()->get('width');
                $request->height = $attachment->getParameters()->get('height');
                $request->caption = $attachment->getParameters()->get('caption');
                break;
        }

        if ($request) {
            $request->chat_id = $chat->getId();
            $request->disable_notification = $attachment->getParameters()->get('disable_notification');
            $request->reply_to_message_id = $attachment->getParameters()->get('reply_to_message_id');
            $request->reply_markup = $attachment->getParameters()->get('reply_markup');

            $this->client->performApiRequest($request);
            $this->loop->run();
        }
    }

    public function __destruct()
    {
        if ($this->loop) {
            // Remove inline keyboard markup after user replied
            if ($this->update && $this->update->callback_query) {
                $request = new EditMessageReplyMarkup();
                $request->chat_id = $this->update->callback_query->message->chat->id;
                $request->message_id = $this->update->callback_query->message->message_id;
                $request->reply_markup = [];

                $this->client->performApiRequest($request);
            }

            $this->loop->run();
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Supplier\EventListener;

use Ekyna\Component\Commerce\Exception;
use Ekyna\Component\Commerce\Stock\Linker\StockUnitLinkerInterface;
use Ekyna\Component\Commerce\Stock\Updater\StockUnitUpdaterInterface;
use Ekyna\Component\Commerce\Supplier\Event\SupplierOrderEvents;
use Ekyna\Component\Commerce\Supplier\Model;
use Ekyna\Component\Resource\Model\ResourceInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

/**
 * Class AbstractListener
 * @package Ekyna\Component\Commerce\Supplier\EventListener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractListener
{
    /**
     * @var PersistenceHelperInterface
     */
    protected $persistenceHelper;

    /**
     * @var StockUnitLinkerInterface
     */
    protected $stockUnitLinker;

    /**
     * @var StockUnitUpdaterInterface
     */
    protected $stockUnitUpdater;

    /**
     * Sets the persistence helper.
     *
     * @param PersistenceHelperInterface $helper
     */
    public function setPersistenceHelper(PersistenceHelperInterface $helper)
    {
        $this->persistenceHelper = $helper;
    }

    /**
     * Sets the stock unit linker.
     *
     * @param StockUnitLinkerInterface $stockUnitLinker
     */
    public function setStockUnitLinker(StockUnitLinkerInterface $stockUnitLinker)
    {
        $this->stockUnitLinker = $stockUnitLinker;
    }

    /**
     * Sets the stock unit updater.
     *
     * @param StockUnitUpdaterInterface $updater
     */
    public function setStockUnitUpdater(StockUnitUpdaterInterface $updater)
    {
        $this->stockUnitUpdater = $updater;
    }

    /**
     * Asserts that the resource can be safely deleted.
     *
     * @param ResourceInterface $resource
     *
     * @throws Exception\CommerceExceptionInterface
     */
    protected function assertDeletable(ResourceInterface $resource)
    {
        if ($resource instanceof Model\SupplierOrderItemInterface) {
            if (null === $stockUnit = $resource->getStockUnit()) {
                return;
            }
            if (0 < $stockUnit->getShippedQuantity()) {
                throw new Exception\IllegalOperationException(
                    "Supplier delivery can't be removed as at least one ".
                    "of its items is linked to a shipped stock unit."
                ); // TODO message as translation id
            }
        } elseif ($resource instanceof Model\SupplierOrderInterface) {
            foreach ($resource->getItems() as $item) {
                $this->assertDeletable($item);
            }
        } elseif ($resource instanceof Model\SupplierDeliveryItemInterface) {
            $this->assertDeletable($resource->getOrderItem());
        } elseif ($resource instanceof Model\SupplierDeliveryInterface) {
            foreach ($resource->getItems() as $item) {
                $this->assertDeletable($item);
            }
        } else {
            throw new Exception\InvalidArgumentException("Unexpected resource."); // TODO message as translation id
        }
    }

    /**
     * Schedules the supplier order content change event.
     *
     * @param Model\SupplierOrderInterface $order
     */
    protected function scheduleSupplierOrderContentChangeEvent(Model\SupplierOrderInterface $order)
    {
        $this->persistenceHelper->scheduleEvent(SupplierOrderEvents::CONTENT_CHANGE, $order);
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Model;

/**
 * Trait StockSubjectTrait
 * @package Ekyna\Component\Commerce\Stock\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
trait StockSubjectTrait
{
    /**
     * @var string
     */
    protected $stockMode;

    /**
     * @var string
     */
    protected $stockState;

    /**
     * @var float
     */
    protected $stockFloor;

    /**
     * @var float
     */
    protected $inStock;

    /**
     * @var float
     */
    protected $availableStock;

    /**
     * @var float
     */
    protected $virtualStock;

    /**
     * @var \DateTime
     */
    protected $estimatedDateOfArrival;

    /**
     * @var int
     */
    protected $replenishmentTime;

    /**
     * @var string
     */
    protected $geocode;

    /**
     * @var float
     */
    protected $minimumOrderQuantity;

    /**
     * @var bool
     */
    protected $quoteOnly;

    /**
     * @var bool
     */
    protected $endOfLife;

    /**
     * Initializes the stock.
     */
    protected function initializeStock()
    {
        $this->stockMode = StockSubjectModes::MODE_AUTO;
        $this->stockState = StockSubjectStates::STATE_OUT_OF_STOCK;
        $this->stockFloor = 0;
        $this->inStock = 0;
        $this->availableStock = 0;
        $this->virtualStock = 0;
        $this->replenishmentTime = 2;
        $this->minimumOrderQuantity = 1;
        $this->quoteOnly = false;
        $this->endOfLife = false;
    }

    /**
     * Returns the stock mode.
     *
     * @return string
     */
    public function getStockMode()
    {
        return $this->stockMode;
    }

    /**
     * Sets the stock mode.
     *
     * @param string $mode
     *
     * @return $this|StockSubjectInterface
     */
    public function setStockMode($mode)
    {
        $this->stockMode = $mode;

        return $this;
    }

    /**
     * Returns the stock state.
     *
     * @return string
     */
    public function getStockState()
    {
        return $this->stockState;
    }

    /**
     * Sets the stock state.
     *
     * @param string $state
     *
     * @return $this|StockSubjectInterface
     */
    public function setStockState($state)
    {
        $this->stockState = $state;

        return $this;
    }

    /**
     * Returns the stock floor.
     *
     * @return float
     */
    public function getStockFloor()
    {
        return $this->stockFloor;
    }

    /**
     * Sets the stock floor.
     *
     * @param float $floor
     *
     * @return $this|StockSubjectInterface
     */
    public function setStockFloor($floor)
    {
        $this->stockFloor = $floor;

        return $this;
    }

    /**
     * Returns the in stock quantity.
     *
     * @return float
     */
    public function getInStock()
    {
        return $this->inStock;
    }

    /**
     * Sets the in stock quantity.
     *
     * @param float $quantity
     *
     * @return $this|StockSubjectInterface
     */
    public function setInStock($quantity)
    {
        $this->inStock = (float)$quantity;

        return $this;
    }

    /**
     * Returns the available stock quantity.
     *
     * @return float
     */
    public function getAvailableStock()
    {
        return $this->availableStock;
    }

    /**
     * Sets the available stock quantity.
     *
     * @param float $quantity
     *
     * @return $this|StockSubjectInterface
     */
    public function setAvailableStock($quantity)
    {
        $this->availableStock = (float)$quantity;

        return $this;
    }

    /**
     * Returns the virtual stock quantity.
     *
     * @return float
     */
    public function getVirtualStock()
    {
        return $this->virtualStock;
    }

    /**
     * Sets the virtual stock quantity.
     *
     * @param float $quantity
     *
     * @return $this|StockSubjectInterface
     */
    public function setVirtualStock($quantity)
    {
        $this->virtualStock = (float)$quantity;

        return $this;
    }

    /**
     * Returns the estimated date of arrival.
     *
     * @return \DateTime
     */
    public function getEstimatedDateOfArrival()
    {
        return $this->estimatedDateOfArrival;
    }

    /**
     * Sets the estimated date of arrival.
     *
     * @param \DateTime $date
     *
     * @return $this|StockSubjectInterface
     */
    public function setEstimatedDateOfArrival(\DateTime $date = null)
    {
        $this->estimatedDateOfArrival = $date;

        return $this;
    }

    /**
     * Returns the replenishment time.
     *
     * @return int
     */
    public function getReplenishmentTime()
    {
        return $this->replenishmentTime;
    }

    /**
     * Sets the replenishment time.
     *
     * @param int $days
     *
     * @return $this|StockSubjectInterface
     */
    public function setReplenishmentTime($days)
    {
        $this->replenishmentTime = (int)$days;

        return $this;
    }

    /**
     * Returns the geocode.
     *
     * @return string
     */
    public function getGeocode()
    {
        return $this->geocode;
    }

    /**
     * Sets the geocode.
     *
     * @param string $code
     *
     * @return $this|StockSubjectInterface
     */
    public function setGeocode($code)
    {
        $this->geocode = $code;

        return $this;
    }

    /**
     * Returns the minimum order quantity.
     *
     * @return float
     */
    public function getMinimumOrderQuantity()
    {
        return $this->minimumOrderQuantity;
    }

    /**
     * Sets the minimum order quantity.
     *
     * @param float $quantity
     *
     * @return $this|StockSubjectInterface
     */
    public function setMinimumOrderQuantity($quantity)
    {
        $this->minimumOrderQuantity = $quantity;

        return $this;
    }

    /**
     * Returns whether or not this subject is available only through quotes.
     *
     * @return bool
     */
    public function isQuoteOnly()
    {
        return $this->quoteOnly;
    }

    /**
     * Sets the whether or not this subject is available only through quotes.
     *
     * @param bool $quoteOnly
     *
     * @return $this|StockSubjectInterface
     */
    public function setQuoteOnly($quoteOnly)
    {
        $this->quoteOnly = (bool)$quoteOnly;

        return $this;
    }

    /**
     * Returns the endOfLife.
     *
     * @return bool
     */
    public function isEndOfLife()
    {
        return $this->endOfLife;
    }

    /**
     * Sets the endOfLife.
     *
     * @param bool $endOfLife
     *
     * @return $this|StockSubjectInterface
     */
    public function setEndOfLife($endOfLife)
    {
        $this->endOfLife = (bool)$endOfLife;

        return $this;
    }

    /**
     * Returns whether the subject is compound (ie stock is resolved regarding to children).
     *
     * @return bool
     */
    public function isStockCompound()
    {
        return false;
    }
}