<?php

namespace Ekyna\Component\Commerce\Invoice\EventListener;

use Ekyna\Component\Commerce\Exception;
use Ekyna\Component\Commerce\Invoice\Model;
use Ekyna\Component\Commerce\Stock\Assigner\StockUnitAssignerInterface;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

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

    /**
     * @var StockUnitAssignerInterface
     */
    protected $stockUnitAssigner;

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

    /**
     * Sets the stock assigner.
     *
     * @param StockUnitAssignerInterface $stockUnitAssigner
     */
    public function setStockUnitAssigner(StockUnitAssignerInterface $stockUnitAssigner)
    {
        $this->stockUnitAssigner = $stockUnitAssigner;
    }

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

        $this->stockUnitAssigner->assignInvoiceLine($line);

        $this->scheduleInvoiceContentChangeEvent($line->getInvoice());
    }

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

        $this->preventForbiddenChange($line);

        $this->stockUnitAssigner->applyInvoiceLine($line);

        // If invoice item quantity has changed
        if ($this->persistenceHelper->isChanged($line, 'quantity')) {
            $this->scheduleInvoiceContentChangeEvent($line->getInvoice());
        }
    }

    /**
     * Delete event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onDelete(ResourceEventInterface $event)
    {
        $line = $this->getInvoiceLineFromEvent($event);

        $this->stockUnitAssigner->detachInvoiceLine($line);

        // Get invoice from change set if null
        if (null === $invoice = $line->getInvoice()) {
            $invoice = $this->persistenceHelper->getChangeSet($line, 'invoice')[0];
        }

        $this->scheduleInvoiceContentChangeEvent($invoice);
    }

    /**
     * Prevents some of the invoice line fields from changing.
     *
     * @param Model\InvoiceLineInterface $line
     */
    protected function preventForbiddenChange(Model\InvoiceLineInterface $line)
    {
        if ($this->persistenceHelper->isChanged($line, 'type')) {
            list($old, $new) = $this->persistenceHelper->getChangeSet($line, 'type');
            if ($old !== $new) {
                throw new Exception\RuntimeException("Changing the invoice line's type is not supported.");
            }
        }
    }

    /**
     * Schedules the invoice content change event.
     *
     * @param Model\InvoiceInterface $invoice
     */
    abstract protected function scheduleInvoiceContentChangeEvent(Model\InvoiceInterface $invoice);

    /**
     * Returns the invoice line from the event.
     *
     * @param ResourceEventInterface $event
     *
     * @return Model\InvoiceLineInterface
     * @throws Exception\InvalidArgumentException
     */
    abstract protected function getInvoiceLineFromEvent(ResourceEventInterface $event);
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints;

use Ekyna\Component\Commerce\Cart\Model\CartInterface;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class SaleShipmentStepValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleShipmentStepValidator extends ConstraintValidator
{
    /**
     * @inheritdoc
     */
    public function validate($sale, Constraint $constraint)
    {
        if (null === $sale) {
            return;
        }

        if (!$sale instanceof SaleInterface) {
            throw new UnexpectedTypeException($sale, SaleInterface::class);
        }
        if (!$constraint instanceof SaleShipmentStep) {
            throw new UnexpectedTypeException($constraint, SaleShipmentStep::class);
        }

        if ($sale instanceof CartInterface && $sale->isLocked()) {
            $this->context
                ->buildViolation($constraint->cart_is_locked)
                ->addViolation();

            return;
        }

        if (!$this->isIdentityValid($sale)) {
            $this->context
                ->buildViolation($constraint->identity_must_be_set)
                ->addViolation();
        }
        if (null === $sale->getInvoiceAddress()) {
            $this->context
                ->buildViolation($constraint->invoice_address_must_be_set)
                ->addViolation();
        }
        if (!$sale->isSameAddress() && null === $sale->getDeliveryAddress()) {
            $this->context
                ->buildViolation($constraint->delivery_address_must_be_set)
                ->addViolation();
        }
    }

    /**
     * Returns whether the sale identity fields are valid.
     *
     * @param SaleInterface $cart
     *
     * @return bool
     */
    private function isIdentityValid(SaleInterface $cart)
    {
        return 0 < strlen($cart->getEmail())
            && 0 < strlen($cart->getGender())
            && 0 < strlen($cart->getFirstName())
            && 0 < strlen($cart->getLastName());
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: jesusslim
 * Date: 16/7/25
 * Time: 下午5:44
 */

namespace Inject;

use Closure;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use ArrayAccess;
class Injector implements InjectorInterface,ArrayAccess
{

    const INDEX_CONCRETE = 0; //the concrete
    const INDEX_CACHED = 1; //cached it after produce
    protected $objects = []; //the objects not instantiated when map or closures
    protected $caches = []; //caches of the produced objects
    protected $data = []; //the objects instantiated when map
    protected $MUST_REG = false; //is MUST_REG is true,Inject won't produce the concrete unmapped

    /**
     * set MUST_REG
     */
    public function mustReg(){
        $this->MUST_REG = true;
    }

    /**
     * map an object that not instantiated
     * @param $key
     * @param $obj
     * @param bool $need_cache
     * @return $this
     */
    public function map($key,$obj = null,$need_cache = false){
        $this->clearCache($key);
        if (is_null($obj)) {
            $obj = $key;
        }
        $this->objects[$key] = [$obj,$need_cache];
        return $this;
    }

    /**
     * map an instantiated object
     * @param $key
     * @param $data
     */
    public function mapData($key,$data){
        $this->data[$key] = $data;
    }

    /**
     * map instantiated objects
     * @param $kvs
     */
    public function mapDatas($kvs){
        foreach ($kvs as $k => $v){
            $this->mapData($k,$v);
        }
    }

    /**
     * map an object that not instantiated and cache it after produce
     * @param $key
     * @param null $class
     * @return Injector
     */
    public function mapSingleton($key,$class = null){
        return $this->map($key,$class,true);
    }

    /**
     * map singletons
     * @param $kvs
     */
    public function mapSingletons($kvs){
        foreach ($kvs as $k => $v){
            $this->mapSingleton($k,$v);
        }
    }

    /**
     * get an object
     * @param $key
     * @return mixed
     * @throws InjectorException
     */
    public function get($key){
        if(isset($this->objects[$key])){
            return $this->objects[$key];
        }
        throw new InjectorException("obj $key not found");
    }

    /**
     * clear the cached objects
     * @param $key
     */
    public function clearCache($key){
        unset($this->caches[$key]);
    }

    /**
     * get an instantiated object mapped
     * @param $key
     * @return mixed
     * @throws InjectorException
     */
    public function getData($key){
        if(isset($this->data[$key])){
            return $this->data[$key];
        }
        throw new InjectorException("data $key not found");
    }

    /**
     * get a cached object
     * @param $key
     * @return mixed|null
     */
    public function getCache($key){
        return isset($this->caches[$key]) ? $this->caches[$key] : null;
    }

    /**
     * produce a concrete
     * @param $key
     * @param $params
     * @param bool $enable_reflect if $enable_reflect is true,it won't try to reflect for an unmapped concrete
     * @return mixed|object
     * @throws InjectorException
     */
    public function produce($key,$params = array(),$enable_reflect = true){
        //if in data
        if(isset($this->data[$key])) return $this->data[$key];
        //if cached
        if(isset($this->caches[$key])) return $this->caches[$key];
        //if obj/closure
        if(isset($this->objects[$key])){
            $obj = $this->get($key);
            $concrete = $obj[self::INDEX_CONCRETE];
        }else{
            if($this->MUST_REG || !$enable_reflect){
                throw new InjectorException("$key not registered");
            }else{
                $concrete = $key;
                $not_reg = true;
            }
        }
        $result = $this->build($concrete,$params);
        if($not_reg === true || $obj[self::INDEX_CACHED] === true){
            $this->caches[$key] = $result;
        }
        return $result;
    }

    /**
     * build concrete (a Closure or an object)
     * @param $concrete
     * @param array $params
     * @return object
     * @throws InjectorException
     */
    public function build($concrete,$params = array()){
        //if closure
        if($concrete instanceof Closure){
            return $this->call($concrete,$params);
//            return $concrete($this,$params);
        }
        //reflect
        $ref = new ReflectionClass($concrete);
        if(!$ref->isInstantiable()) throw new InjectorException("$concrete is not instantiable");
        $constructor = $ref->getConstructor();
        if(is_null($constructor)) return new $concrete;
        //constructor
        $params_in_constructor = $constructor->getParameters();
        $args = $this->apply($params_in_constructor,$params);
        return $ref->newInstanceArgs($args);
    }

    /**
     * fill the params(keys) by the values given and objects in Injector container
     * @param array $params
     * @param array $value_given
     * @return array
     * @throws InjectorException
     */
    public function apply(array $params,$value_given = array()){
        $result = array();
        foreach ($params as $param){
            if(key_exists($param->name,$value_given)){
                $result[] = $value_given[$param->name];
            }else{
                $class = $param->getClass();
                $name_to_produce = is_null($class) ? $param->name : $class->name;
                try{
                    $temp = $this->produce($name_to_produce,array(),false);
                }catch (InjectorException $e){
                    if($param->isDefaultValueAvailable()){
                        $temp = $param->getDefaultValue();
                    }else{
                        throw $e;
                    }
                }
                $result[] = $temp;
            }
        }
        return $result;
    }

    /**
     * call a closure
     * @param Closure $c
     * @param array $params
     * @return mixed
     */
    public function call(Closure $c,$params = array()){
        $ref = new ReflectionFunction($c);
        $params_need = $ref->getParameters();
        $args = $this->apply($params_need,$params);
        return call_user_func_array($c,$args);
        //return $ref->invokeArgs($args);
    }

    /**
     * call a method in class
     * @param $class_name
     * @param $action
     * @param array $params
     * @return mixed
     * @throws InjectorException
     */
    public function callInClass($class_name,$action,$params = array()){
        $ref = new ReflectionMethod($class_name, $action);
        if(!$ref->isPublic() && !$ref->isStatic()) throw new InjectorException("$class_name->$action is not public or static");
        $params_need = $ref->getParameters();
        $args = $this->apply($params_need,$params);
        $obj = $this->produce($class_name,$params);
        return call_user_func_array([$obj,$action],$args);
        //return $ref->invokeArgs($obj,$args);
    }

    /**** implement methods of ArrayAccess ****/

    public function offsetExists($offset)
    {
        return isset($this->objects[$offset]) || isset($this->caches[$offset]) || isset($this->data[$offset]);
    }

    public function offsetGet($offset)
    {
        return $this->produce($offset);
    }

    public function offsetSet($offset, $value)
    {
        if (! $value instanceof Closure){
            $value = function () use ($value) {
                return $value;
            };
        }
        $this->map($offset,$value);
    }

    public function offsetUnset($offset)
    {
        unset($this->objects[$offset],$this->caches[$offset],$this->data[$offset]);
    }

    public function flush(){
        $this->objects = [];
        $this->data = [];
        $this->caches = [];
    }
}


<?php

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

use Ekyna\Component\Commerce\Bridge\Payum\CreditBalance\Constants;
use Ekyna\Component\Commerce\Exception\RuntimeException;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\Convert;

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

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

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

        if (isset($details[Constants::FIELD_STATUS])) {
            return;
        }

        if (null === $sale = $payment->getSale()) {
            throw new RuntimeException("Payment's sale must be defined at this point.");
        }
        if (null === $customer = $sale->getCustomer()) {
            throw new RuntimeException("Sale's customer must be defined at this point.");
        }

        $details->defaults(array(
            Constants::FIELD_STATUS  => null,
            Constants::FIELD_AMOUNT  => $payment->getAmount(),
            Constants::FIELD_BALANCE => $customer->getCreditBalance(),
        ));

        $request->setResult((array) $details);
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return $request instanceof Convert
            && $request->getSource() instanceof PaymentInterface
            && $request->getTo() == 'array';
    }
}


<?php

/**
 * Ohanzee Components by Kohana
 *
 * @package    Ohanzee
 * @author     Kohana Team <team@kohanaframework.org>
 * @copyright  2007-2014 Kohana Team
 * @link       http://ohanzee.org/
 * @license    http://ohanzee.org/license
 * @version    0.1.0
 *
 * BSD 2-CLAUSE LICENSE
 *
 * This license is a legal agreement between you and the Kohana Team for the use
 * of Kohana Framework and Ohanzee Components (the "Software"). By obtaining the
 * Software you agree to comply with the terms and conditions of this license.
 *
 * Copyright (c) 2007-2014 Kohana Team
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1) Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2) Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace mii\util;

class Date
{
    // Second amounts for various time increments
    const YEAR = 31556926;
    const MONTH = 2629744;
    const WEEK = 604800;
    const DAY = 86400;
    const HOUR = 3600;
    const MINUTE = 60;

    // Available formats for Date::months()
    const MONTHS_LONG = '%B';
    const MONTHS_SHORT = '%b';

    /**
     * Default timestamp format for formatted_time
     * @var  string
     */
    public static $timestamp_format = 'Y-m-d H:i:s';

    /**
     * Timezone for formatted_time
     * @link http://uk2.php.net/manual/en/timezones.php
     * @var  string
     */
    public static $timezone;

    /**
     * Returns the offset (in seconds) between two time zones. Use this to
     * display dates to users in different time zones.
     *
     *     $seconds = Date::offset('America/Chicago', 'GMT');
     *
     * [!!] A list of time zones that PHP supports can be found at
     * <http://php.net/timezones>.
     *
     * @param string $remote timezone that to find the offset of
     * @param string $local timezone used as the baseline
     * @param mixed $now UNIX timestamp or date string
     *
     * @return integer
     */
    public static function offset($remote, $local = null, $now = null) {
        if ($local === null) {
            // Use the default timezone
            $local = date_default_timezone_get();
        }

        if (is_int($now)) {
            // Convert the timestamp into a string
            $now = date(\DateTime::RFC2822, $now);
        }

        // Create timezone objects
        $zone_remote = new \DateTimeZone($remote);
        $zone_local = new \DateTimeZone($local);

        // Create date objects from timezones
        $time_remote = new \DateTime($now, $zone_remote);
        $time_local = new \DateTime($now, $zone_local);

        // Find the offset
        $offset = $zone_remote->getOffset($time_remote) - $zone_local->getOffset($time_local);

        return $offset;
    }

    /**
     * Number of seconds in a minute, incrementing by a step. Typically used as
     * a shortcut for generating a list that can used in a form.
     *
     *     $seconds = Date::seconds(); // 01, 02, 03, ..., 58, 59, 60
     *
     * @param   integer $step amount to increment each step by, 1 to 30
     * @param   integer $start start value
     * @param   integer $end end value
     * @return  array   A mirrored (foo => foo) array from 1-60.
     */
    public static function seconds($step = 1, $start = 0, $end = 60) {
        // Always integer
        $step = (int)$step;

        $seconds = array();
        for ($i = $start; $i < $end; $i += $step) {
            $seconds[$i] = sprintf('%02d', $i);
        }

        return $seconds;
    }

    /**
     * Number of minutes in an hour, incrementing by a step. Typically used as
     * a shortcut for generating a list that can be used in a form.
     *
     *     $minutes = Date::minutes(); // 05, 10, 15, ..., 50, 55, 60
     *
     * @uses    Date::seconds
     * @param   integer $step amount to increment each step by, 1 to 30
     * @return  array   A mirrored (foo => foo) array from 1-60.
     */
    public static function minutes($step = 5) {
        // Because there are the same number of minutes as seconds in this set,
        // we choose to re-use seconds(), rather than creating an entirely new
        // function. Shhhh, it's cheating! ;) There are several more of these
        // in the following methods.
        return static::seconds($step);
    }

    /**
     * Number of hours in a day. Typically used as a shortcut for generating a
     * list that can be used in a form.
     *
     *     $hours = Date::hours(); // 01, 02, 03, ..., 10, 11, 12
     *
     * @param   integer $step amount to increment each step by
     * @param   boolean $long use 24-hour time
     * @param   integer $start the hour to start at
     * @return  array   A mirrored (foo => foo) array from start-12 or start-23.
     */
    public static function hours($step = 1, $long = false, $start = null) {
        // Set the default start if none was specified.
        if (!$start) {
            $start = $long ? 0 : 1;
        }

        // 24-hour time has 24 hours, instead of 12
        $size = $long ? 23 : 12;
        $step = (int)$step;

        $hours = array();
        for ($i = $start; $i <= $size; $i += $step) {
            $hours[$i] = (string)$i;
        }

        return $hours;
    }

    /**
     * Returns AM or PM, based on a given hour (in 24 hour format).
     *
     *     $type = Date::ampm(12); // PM
     *     $type = Date::ampm(1);  // AM
     *
     * @param   integer $hour number of the hour
     * @return  string
     */
    public static function ampm($hour) {
        // Always integer
        $hour = (int)$hour;

        return ($hour > 11) ? 'PM' : 'AM';
    }

    /**
     * Adjusts a non-24-hour number into a 24-hour number.
     *
     *     $hour = Date::adjust(3, 'pm'); // 15
     *
     * @param   integer $hour hour to adjust
     * @param   string $ampm AM or PM
     * @return  string
     */
    public static function adjust($hour, $ampm) {
        $hour = (int)$hour;
        $ampm = strtolower($ampm);

        switch ($ampm) {
            case 'am':
                if ($hour == 12) {
                    $hour = 0;
                }
                break;
            case 'pm':
                if ($hour < 12) {
                    $hour += 12;
                }
                break;
        }

        return sprintf('%02d', $hour);
    }

    /**
     * Number of days in a given month and year. Typically used as a shortcut
     * for generating a list that can be used in a form.
     *
     *     Date::days(4, 2010); // 1, 2, 3, ..., 28, 29, 30
     *
     * @param   integer $month number of month
     * @param   integer $year number of year to check month, defaults to the current year
     * @return  array   A mirrored (foo => foo) array of the days.
     */
    public static function days($month, $year = null) {
        static $months;

        if (!isset($year)) {
            // Use the current year by default
            $year = date('Y');
        }

        // Always integers
        $month = (int)$month;
        $year = (int)$year;

        // We use caching for months, because time functions are used
        if (empty($months[$year][$month])) {
            // Use date to find the number of days in the given month
            $total = date('t', mktime(1, 0, 0, $month, 1, $year)) + 1;

            $months[$year][$month] = array();
            for ($i = 1; $i < $total; $i++) {
                $months[$year][$month][$i] = (string)$i;
            }
        }

        return $months[$year][$month];
    }

    /**
     * Number of months in a year. Typically used as a shortcut for generating
     * a list that can be used in a form.
     *
     * By default a mirrored array of $month_number => $month_number is returned
     *
     *     Date::months();
     *     // aray(1 => 1, 2 => 2, 3 => 3, ..., 12 => 12)
     *
     * But you can customise this by passing in either Date::MONTHS_LONG
     *
     *     Date::months(Date::MONTHS_LONG);
     *     // array(1 => 'January', 2 => 'February', ..., 12 => 'December')
     *
     * Or Date::MONTHS_SHORT
     *
     *     Date::months(Date::MONTHS_SHORT);
     *     // array(1 => 'Jan', 2 => 'Feb', ..., 12 => 'Dec')
     *
     * @uses    Date::hours
     * @param   string $format The format to use for months
     * @return  array   An array of months based on the specified format
     */
    public static function months($format = null) {
        $months = array();

        if ($format === static::MONTHS_LONG || $format === static::MONTHS_SHORT) {
            for ($i = 1; $i <= 12; ++$i) {
                $months[$i] = strftime($format, mktime(0, 0, 0, $i, 1));
            }
        } else {
            $months = static::hours();
        }

        return $months;
    }

    /**
     * Returns an array of years between a starting and ending year. By default,
     * the the current year - 5 and current year + 5 will be used. Typically used
     * as a shortcut for generating a list that can be used in a form.
     *
     *     $years = Date::years(2000, 2010); // 2000, 2001, ..., 2009, 2010
     *
     * @param   integer $start starting year (default is current year - 5)
     * @param   integer $end ending year (default is current year + 5)
     * @return  array
     */
    public static function years($start = false, $end = false) {
        // Default values
        $start = ($start === false) ? (date('Y') - 5) : (int)$start;
        $end = ($end === false) ? (date('Y') + 5) : (int)$end;

        $years = array();
        for ($i = $start; $i <= $end; $i++) {
            $years[$i] = (string)$i;
        }

        return $years;
    }

    /**
     * Returns time difference between two timestamps, in human readable format.
     * If the second timestamp is not given, the current time will be used.
     * Also consider using [Date::fuzzy_span] when displaying a span.
     *
     *     $span = Date::span(60, 182, 'minutes,seconds'); // array('minutes' => 2, 'seconds' => 2)
     *     $span = Date::span(60, 182, 'minutes'); // 2
     *
     * @param   integer $remote timestamp to find the span of
     * @param   integer $local timestamp to use as the baseline
     * @param   string $output formatting string
     * @return  string   when only a single output is requested
     * @return  array    associative list of all outputs requested
     */
    public static function span($remote, $local = null, $output = 'years,months,weeks,days,hours,minutes,seconds') {
        // Normalize output
        $output = trim(strtolower((string)$output));

        if (!$output) {
            // Invalid output
            return false;
        }

        // Array with the output formats
        $output = preg_split('/[^a-z]+/', $output);

        // Convert the list of outputs to an associative array
        $output = array_combine($output, array_fill(0, count($output), 0));

        // Make the output values into keys
        extract(array_flip($output), EXTR_SKIP);

        if ($local === null) {
            // Calculate the span from the current time
            $local = time();
        }

        // Calculate timespan (seconds)
        $timespan = abs($remote - $local);

        if (isset($output['years'])) {
            $timespan -= static::YEAR * ($output['years'] = (int)floor($timespan / static::YEAR));
        }

        if (isset($output['months'])) {
            $timespan -= static::MONTH * ($output['months'] = (int)floor($timespan / static::MONTH));
        }

        if (isset($output['weeks'])) {
            $timespan -= static::WEEK * ($output['weeks'] = (int)floor($timespan / static::WEEK));
        }

        if (isset($output['days'])) {
            $timespan -= static::DAY * ($output['days'] = (int)floor($timespan / static::DAY));
        }

        if (isset($output['hours'])) {
            $timespan -= static::HOUR * ($output['hours'] = (int)floor($timespan / static::HOUR));
        }

        if (isset($output['minutes'])) {
            $timespan -= static::MINUTE * ($output['minutes'] = (int)floor($timespan / static::MINUTE));
        }

        // Seconds ago, 1
        if (isset($output['seconds'])) {
            $output['seconds'] = $timespan;
        }

        if (count($output) === 1) {
            // Only a single output was requested, return it
            return array_pop($output);
        }

        // Return array
        return $output;
    }

    /**
     * Returns the difference between a time and now in a "fuzzy" way.
     * Displaying a fuzzy time instead of a date is usually faster to read and understand.
     *
     *     $span = Date::fuzzy_span(time() - 10); // "moments ago"
     *     $span = Date::fuzzy_span(time() + 20); // "in moments"
     *
     * A second parameter is available to manually set the "local" timestamp,
     * however this parameter shouldn't be needed in normal usage and is only
     * included for unit tests
     *
     * @param   integer $timestamp "remote" timestamp
     * @param   integer $local_timestamp "local" timestamp, defaults to time()
     * @return  string
     */
    public static function fuzzySpan($timestamp, $local_timestamp = null) {
        $local_timestamp = ($local_timestamp === null) ? time() : (int)$local_timestamp;

        // Determine the difference in seconds
        $offset = abs($local_timestamp - $timestamp);

        if ($offset <= static::MINUTE) {
            $span = 'moments';
        } elseif ($offset < (static::MINUTE * 20)) {
            $span = 'a few minutes';
        } elseif ($offset < static::HOUR) {
            $span = 'less than an hour';
        } elseif ($offset < (static::HOUR * 4)) {
            $span = 'a couple of hours';
        } elseif ($offset < static::DAY) {
            $span = 'less than a day';
        } elseif ($offset < (static::DAY * 2)) {
            $span = 'about a day';
        } elseif ($offset < (static::DAY * 4)) {
            $span = 'a couple of days';
        } elseif ($offset < static::WEEK) {
            $span = 'less than a week';
        } elseif ($offset < (static::WEEK * 2)) {
            $span = 'about a week';
        } elseif ($offset < static::MONTH) {
            $span = 'less than a month';
        } elseif ($offset < (static::MONTH * 2)) {
            $span = 'about a month';
        } elseif ($offset < (static::MONTH * 4)) {
            $span = 'a couple of months';
        } elseif ($offset < static::YEAR) {
            $span = 'less than a year';
        } elseif ($offset < (static::YEAR * 2)) {
            $span = 'about a year';
        } elseif ($offset < (static::YEAR * 4)) {
            $span = 'a couple of years';
        } elseif ($offset < (static::YEAR * 8)) {
            $span = 'a few years';
        } elseif ($offset < (static::YEAR * 12)) {
            $span = 'about a decade';
        } elseif ($offset < (static::YEAR * 24)) {
            $span = 'a couple of decades';
        } elseif ($offset < (static::YEAR * 64)) {
            $span = 'several decades';
        } else {
            $span = 'a long time';
        }

        if ($timestamp <= $local_timestamp) {
            // This is in the past
            return $span . ' ago';
        } else {
            // This in the future
            return 'in ' . $span;
        }
    }

    /**
     * Converts a UNIX timestamp to DOS format. There are very few cases where
     * this is needed, but some binary formats use it (eg: zip files.)
     * Converting the other direction is done using {@link Date::dos2unix}.
     *
     *     $dos = Date::unix2dos($unix);
     *
     * @param   integer $timestamp UNIX timestamp
     * @return  integer
     */
    public static function unix2dos($timestamp = null) {
        $timestamp = getdate($timestamp);

        if ($timestamp['year'] < 1980) {
            return (1 << 21 | 1 << 16);
        }

        $timestamp['year'] -= 1980;

        // What voodoo is this? I have no idea... Geert can explain it though,
        // and that's good enough for me.
        return ($timestamp['year'] << 25 | $timestamp['mon'] << 21 |
            $timestamp['mday'] << 16 | $timestamp['hours'] << 11 |
            $timestamp['minutes'] << 5 | $timestamp['seconds'] >> 1);
    }

    /**
     * Converts a DOS timestamp to UNIX format.There are very few cases where
     * this is needed, but some binary formats use it (eg: zip files.)
     * Converting the other direction is done using {@link Date::unix2dos}.
     *
     *     $unix = Date::dos2unix($dos);
     *
     * @param   integer $timestamp DOS timestamp
     * @return  integer
     */
    public static function dos2unix($timestamp = false) {
        $sec = 2 * ($timestamp & 0x1f);
        $min = ($timestamp >> 5) & 0x3f;
        $hrs = ($timestamp >> 11) & 0x1f;
        $day = ($timestamp >> 16) & 0x1f;
        $mon = ($timestamp >> 21) & 0x0f;
        $year = ($timestamp >> 25) & 0x7f;

        return mktime($hrs, $min, $sec, $mon, $day, $year + 1980);
    }

    /**
     * Returns a date/time string with the specified timestamp format
     *
     *     $time = Date::formatted_time('5 minutes ago');
     *
     * @link    http://www.php.net/manual/\DateTime.construct
     * @param   string $\DateTime_str       \DateTime string
     * @param   string $timestamp_format timestamp format
     * @param   string $timezone timezone identifier
     * @return  string
     */
    public static function formattedTime($DateTime_str = 'now', $timestamp_format = null, $timezone = null) {
        if (!$timestamp_format) {
            $timestamp_format = static::$timestamp_format;
        }

        if (!$timezone) {
            $timezone = static::$timezone;
        }

        $tz = new \DateTimeZone($timezone);
        $time = new \DateTime($DateTime_str, $tz);

        if ($time->getTimeZone()->getName() !== $tz->getName()) {
            $time->setTimeZone($tz);
        }

        return $time->format($timestamp_format);
    }

    public static $ru_months = array('September' => 'сентября', 'November' => 'ноября', 'October' => 'октября', 'December' => 'декабря',
        'January' => 'января', 'February' => 'февраля', 'March' => 'марта', 'April' => 'апреля',
        'May' => 'мая', 'June' => 'июня', 'July' => 'июля', 'August' => 'августа');

    static function strftime_rus($format, $date = FALSE) {
        // Работает точно так же, как и strftime(),
        // только в строке формата может принимать
        // дополнительный аргумент %B2, который будет заменен
        // на русское название месяца в родительном падеже.

        // В остальном правила для $format такие же, как и для strftime().
        // (в связи с этим рекомендуется настроить выполнение скрипта
        // с помощью setlocale: http://php.net/setlocale)

        // Второй аргумент можно передавать как в виде временной метки
        // так и в виде строки типа 2010-05-16 23:48:20
        // функция сама определит, в каком виде передана дата,
        // и проведет преобразование.
        // Если второй аргумент не указан,
        // функция будет работать с текущим временем.

        if (!$date)
            $timestamp = time();

        elseif (!is_numeric($date))
            $timestamp = strtotime($date);

        else
            $timestamp = $date;

        if (strpos($format, '%B2') === FALSE)
            return strftime($format, $timestamp);

        $month_number = date('n', $timestamp);

        switch ($month_number) {
            case 1:
                $rus = 'января';
                break;
            case 2:
                $rus = 'февраля';
                break;
            case 3:
                $rus = 'марта';
                break;
            case 4:
                $rus = 'апреля';
                break;
            case 5:
                $rus = 'мая';
                break;
            case 6:
                $rus = 'июня';
                break;
            case 7:
                $rus = 'июля';
                break;
            case 8:
                $rus = 'августа';
                break;
            case 9:
                $rus = 'сентября';
                break;
            case 10:
                $rus = 'октября';
                break;
            case 11:
                $rus = 'ноября';
                break;
            case 12:
                $rus = 'декабря';
                break;
        }

        $rusformat = str_replace('%B2', $rus, $format);

        return strftime($rusformat, $timestamp);
    }

    public static function formated($timestamp, $local_timestamp = NULL) {
        $local_timestamp = ($local_timestamp === NULL) ? time() : (int)$local_timestamp;

        // Determine the difference in seconds
        $offset = abs($local_timestamp - $timestamp);

        if ($offset <= Date::MINUTE) {
            $span = 'только что';
        } elseif ($offset < Date::HOUR) {
            $minutes = round($offset / 60);

            $span = $minutes . ' ' . Num::decl($minutes, array('минуту', 'минуты', 'минут')) . ' назад';
        } elseif ($offset == Date::HOUR) {
            $span = 'час назад';
        } elseif ($offset < (Date::HOUR * 2)) {
            $span = 'два часа назад';
        } elseif ($offset < Date::DAY) {
            if (date('d', $timestamp) != date('d'))
                $span = 'вчера в ' . date('H:i', $timestamp);
            else
                $span = 'сегодня в ' . date('H:i', $timestamp);
        } elseif ($offset > Date::DAY) {
            if (date('Y', $timestamp) != date('Y'))
                $span = Date::strftime_rus('%e %B2 %Y', $timestamp) . ' в ' . date('H:i', $timestamp);
            else
                $span = Date::strftime_rus('%e %B2', $timestamp) . ' в ' . date('H:i', $timestamp);
        }

        if ($timestamp <= $local_timestamp) {
            // This is in the past
            return $span;
        } else {
            // This in the future
            return 'в ' . $span;
        }
    }

    public static function day_of_week($date = null, $ucfirst = true) {
        $days = [
            'воскресенье', 'понедельник',
            'вторник', 'среда',
            'четверг', 'пятница', 'суббота'
        ];

        $uf_days = [
            'Воскресенье', 'Понедельник',
            'Вторник', 'Среда',
            'Четверг', 'Пятница', 'Суббота'
        ];

        return ($ucfirst) ? $uf_days[date('w', $date)] : $days[date('w', $date)];
    }

    public static function month($date = null, $genitive = true) {
        static $trans_table = ['September' => 'сентябрь', 'November' => 'ноябрь', 'October' => 'октябрь', 'December' => 'декабрь',
            'January' => 'январь', 'February' => 'февраль', 'March' => 'март', 'April' => 'апрель',
            'May' => 'май', 'June' => 'июнь', 'July' => 'июль', 'August' => 'август'];

        static $trans_table_g = ['September' => 'сентября', 'November' => 'ноября', 'October' => 'октября', 'December' => 'декабря',
            'January' => 'января', 'February' => 'февраля', 'March' => 'марта', 'April' => 'апреля',
            'May' => 'мая', 'June' => 'июня', 'July' => 'июля', 'August' => 'августа'];

        $name = date('F', $date);

        if ($genitive) {
            return isset($trans_table_g[$name]) ? $trans_table_g[$name] : $name;
        } else {
            return isset($trans_table[$name]) ? $trans_table[$name] : $name;
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Payment\Model;

use Ekyna\Component\Commerce\Exception\InvalidArgumentException;

/**
 * Class PaymentTermTriggers
 * @package Ekyna\Component\Commerce\Payment\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class PaymentTermTriggers
{
    const TRIGGER_SHIPPED        = 'shipped';
    const TRIGGER_FULLY_SHIPPED  = 'fully_shipped';
    const TRIGGER_INVOICED       = 'invoiced';
    const TRIGGER_FULLY_INVOICED = 'fully_invoiced';

    /**
     * Returns all the triggers.
     *
     * @return array
     */
    static public function getTriggers()
    {
        return [
            static::TRIGGER_SHIPPED,
            static::TRIGGER_FULLY_SHIPPED,
            static::TRIGGER_INVOICED,
            static::TRIGGER_FULLY_INVOICED,
        ];
    }

    /**
     * Returns whether or not the given trigger is valid.
     *
     * @param string $trigger
     * @param bool   $throwException
     *
     * @return bool
     */
    static public function isValidTrigger($trigger, $throwException = true)
    {
        if (in_array($trigger, static::getTriggers(), true)) {
            return true;
        }

        if ($throwException) {
            throw new InvalidArgumentException("Invalid payment term trigger '$trigger'.");
        }

        return false;
    }

    /**
     * Disabled constructor.
     *
     * @codeCoverageIgnore
     */
    final private function __construct()
    {
    }
}


<?php

namespace PeskyCMF\Scaffold\Form;

class WysiwygFormInput extends FormInput {

    /**
     * @var string|null
     */
    protected $relativeImageUploadsFolder;
    /**
     * @var int
     */
    protected $maxImageWidth = 980;
    /**
     * @var int
     */
    protected $maxImageHeight = 2000;
    /**
     * @var null|\Closure
     */
    protected $dataInserts;
    /**
     * @var null|\Closure
     */
    protected $htmlInserts;
    /**
     * @var null|\Closure
     */
    protected $wysiwygConfig;
    /**
     * @var string
     */
    protected $customJsCode = '';
    /**
     * @var string
     */
    protected $wysiwygInitializerFunctionName = 'ScaffoldFormHelper.initWysiwyg';
    /**
     * @var array
    */
    protected $customCssFiles = [];

    public function getType() {
        return static::TYPE_WYSIWYG;
    }

    /**
     * @param $folder - relative path to folder inside public_path()
     * @return $this
     */
    public function setRelativeImageUploadsFolder($folder) {
        $this->relativeImageUploadsFolder = trim($folder, ' /\\');
        return $this;
    }

    /**
     * @return string|null
     */
    public function getAbsoluteImageUploadsFolder() {
        return public_path($this->relativeImageUploadsFolder) . DIRECTORY_SEPARATOR;
    }

    /**
     * @return string
     */
    public function getRelativeImageUploadsUrl() {
        return '/' . str_replace('\\', '/', $this->relativeImageUploadsFolder) . '/';
    }

    /**
     * @return bool
     */
    public function hasImageUploadsFolder() {
        return !empty($this->relativeImageUploadsFolder);
    }

    /**
     * @return int
     */
    public function getMaxImageWidth() {
        return $this->maxImageWidth;
    }

    /**
     * @param int $maxImageWidth
     * @return $this
     */
    public function setMaxImageWidth($maxImageWidth) {
        $this->maxImageWidth = (int)$maxImageWidth;
        return $this;
    }

    /**
     * @return int
     */
    public function getMaxImageHeight() {
        return $this->maxImageHeight;
    }

    /**
     * @param int $maxImageHeight
     * @return $this
     */
    public function setMaxImageHeight($maxImageHeight) {
        $this->maxImageHeight = (int)$maxImageHeight;
        return $this;
    }

    /**
     * @param \Closure $provider - must return array of associative arrays with keys:
     *  - 'code' - php code that returns some text content. Code is inserted using Blade's command '{!! your_code_here !!}'
     *  - 'title' - insert's label to display inside wysiwyg editor
     *  - 'is_block' (optional)
     *          - true: inserted data is separate block (<div>, <p> or some other block element);
     *          - false (default): inserted data is inline (simple text, <span>, etc)
     *  For example: [
     *      'code' => 'insertData()',
     *      'title' => 'Test insert',
     *      'is_block' => false
     *  ]
     * @return $this
     */
    public function setDataInserts(\Closure $provider) {
        $this->dataInserts = $provider;
        return $this;
    }

    /**
     * Create valid single item config for wysiwyg's data inserter plugin
     * @param string $phpCode - php code that returns some text content. Code is inserted using Blade's command '{!! your_code_here !!}'
     * @param string $title - insert's label to display inside wysiwyg editor
     * @param bool $showAsBlock -
     *      - true: inserted data is separate block (<div>, <p> or some other block element);
     *      - false (default): inserted data is inline (simple text, <span>, etc)
     * @return array
     * @throws \InvalidArgumentException
     */
    static public function createDataInsertConfig($phpCode, $title, $showAsBlock = false) {
        if (!is_string($phpCode) || empty(trim($phpCode))) {
            throw new \InvalidArgumentException('$phpCode argument must be a not empty string');
        }
        if (!is_string($title) || empty(trim($title))) {
            throw new \InvalidArgumentException('$title argument must be a not empty string');
        }
        return [
            'code' => $phpCode,
            'title' => $title,
            'is_block' => (bool)$showAsBlock
        ];
    }

    /**
     * Create valid single item config for wysiwyg's data inserter plugin
     * @param string $phpCode - php code that returns some text content. Code is inserted using Blade's command '{!! your_code_here !!}'
     * @param string $title - insert's label to display inside wysiwyg editor
     * @param bool $showAsBlock -
     *  - true: inserted data is separate block (<div>, <p> or some other block element);
     *  - false (default): inserted data is inline (simple text, <span>, etc)
     * @param array $optionsForArguments - list of options for each argument to be passed into $phpCode.
     *  Example: $phpCode = 'printPageData(":page_code", ":page_field")'. Args are: 'page_code' and 'page_field'.
     *  $optionsForArguments should be: array(
     *      'page_code' => [
     *          'label' => 'Page',
     *          'type' => 'select'
     *          'options' => 'http://domain/admin/api/pages/options_for_inserts'
     *      ],
     *      'page_field' => [
     *          'label' => 'Field'
     *          'type' => 'select'
     *          'options' => [
     *              'title' => 'Title',
     *              'content' => 'Text'
     *          ],
     *          'value' => 'content'
     *      ],
     *      'some_text' => [
     *          'label' => 'Page',
     *          'type' => 'text'
     *      ],
     *  )
     *  Types: 'select', 'text', 'checkbox'
     *  Additional options: 'value' (default one), 'checked' (bool, for checkbox)
     *  Resulting insert may look like 'printPageData("home", "content")'
     *  Note that for options loaded via URL, URL will be modified to contain 'pk' URL query argument that holds
     *  current item id loaded into edit form. This is the way to exclude some items from returned options.
     *  For example if we editing item with primary key value '13' then options url will be
     *  'http://domain/admin/api/pages/options_for_inserts?pk=13'
     * @param null|string $widgetTitleTpl - an alternative title template for insert's representation (widget) inside text editor
     *  You can use args from $optionsForArguments to insert into template.
     *  For example template can be like this: ':some_text (:page_code.label / :page_feild.value)'
     *  Args for select is an object with 2 keys: 'label' and 'value', other args are plain text
     * @return array
     * @throws \InvalidArgumentException
     */
    static public function createDataInsertConfigWithArguments($phpCode, $title, $showAsBlock = false, array $optionsForArguments, $widgetTitleTpl = null) {
        $config = static::createDataInsertConfig($phpCode, $title, $showAsBlock);
        $config['args_options'] = $optionsForArguments;
        $config['widget_title_tpl'] = $widgetTitleTpl;
        return $config;
    }

    /**
     * @return array
     * @throws \UnexpectedValueException
     */
    public function getDataInserts() {
        if (!empty($this->dataInserts)) {
            /** @var array $insertsRaw */
            $insertsRaw = call_user_func($this->dataInserts);
            if (!is_array($insertsRaw)) {
                throw new \UnexpectedValueException('InsertsProvider closure must return an array');
            }
            $inserts = [];
            foreach ($insertsRaw as $config) {
                if (!is_array($config) || empty($config['code']) || empty($config['title'])) {
                    throw new \UnexpectedValueException(
                        'InsertsProvider returned invalid data insert config. Confirm that config is array and has keys "code" and "title"'
                    );
                }
                $config['code'] = "@wysiwygInsert({$config['code']})";
                $config['is_block'] = (bool)array_get($config, 'is_block', false);
                $inserts[] = $config;
            }
            return $inserts;
        }
        return [];
    }

    /**
     * @param string $html - HTML code to insert into editor
     * @param string $title - option title
     * @param bool $isBlock
     * @return array
     * @throws \InvalidArgumentException
     */
    static public function createHtmlInsertConfig($html, $title, $isBlock = true) {
        if (!is_string($html) || empty(trim($html))) {
            throw new \InvalidArgumentException('$html argument must be a not empty string');
        }
        if (!is_string($title) || empty(trim($title))) {
            throw new \InvalidArgumentException('$title argument must be a not empty string');
        }
        return [
            'html' => $html,
            'title' => $title,
            'is_block' => (bool)$isBlock
        ];
    }

    /**
     * @param \Closure $provider
     * @return $this
     */
    public function setHtmlInserts(\Closure $provider) {
        $this->htmlInserts = $provider;
        return $this;
    }

    /**
     * @return array
     * @throws \UnexpectedValueException
     */
    public function getHtmlInserts() {
        if (!empty($this->htmlInserts)) {
            /** @var array $insertsRaw */
            $insertsRaw = call_user_func($this->htmlInserts);
            if (!is_array($insertsRaw)) {
                throw new \UnexpectedValueException('InsertsProvider closure must return an array');
            }
            $inserts = [];
            foreach ($insertsRaw as $config) {
                if (!is_array($config) || empty($config['html']) || empty($config['title'])) {
                    throw new \UnexpectedValueException(
                        'InsertsProvider returned invalid data insert config. Confirm that config is array and has keys "html" and "title"'
                    );
                }
                $inserts[] = $config;
            }
            return $inserts;
        }
        return [];
    }

    /**
     * This file will be added into wysiwyg editor to allow custom styling inside editor
     * It also allows to display HtmlInserts the same way as on frontend
     * @param array $cssFiles
     * @return $this
     */
    public function addCssFilesToWysiwygEditor(...$cssFiles) {
        $this->customCssFiles = $cssFiles;
        return $this;
    }

    /**
     * @param \Closure $configMaker - must return array
     * @return $this
     */
    public function setWysiwygConfig(\Closure $configMaker) {
        $this->wysiwygConfig = $configMaker;
        return $this;
    }

    /**
     * @return array
     * @throws \UnexpectedValueException
     */
    public function getWysiwygConfig() {
        $config = [];
        if (!empty($this->wysiwygConfig)) {
            $config = call_user_func($this->wysiwygConfig);
            if (!is_array($config)) {
                throw new \UnexpectedValueException('WysiwygConfig closure must return an array');
            }
        }
        $config['data_inserts'] = $this->getDataInserts();
        $config['html_inserts'] = $this->getHtmlInserts();
        if (!empty($this->customCssFiles)) {
            if (array_key_exists('contentsCss', $config)) {
                $config['contentsCss'] = array_unique(array_merge((array)$config['contentsCss'], $this->customCssFiles));
            } else {
                $config['contentsCss'] = $this->customCssFiles;
            }
        }
        return $config;
    }

    /**
     * @param string $jsCode - valid javascript code. Don't forget to add ';' at the end
     * @return $this
     */
    public function setCustomJsCode($jsCode) {
        $this->customJsCode = (string)$jsCode;
        return $this;
    }

    /**
     * @return string
     */
    public function getCustomJsCode() {
        return $this->customJsCode;
    }

    /**
     * @param string $functionName
     */
    public function setWysiwygInitializerFunctionName($functionName) {
        $this->wysiwygInitializerFunctionName = (string)$functionName;
    }

    /**
     * @return string
     */
    public function getWysiwygInitializerFunctionName() {
        return $this->wysiwygInitializerFunctionName;
    }

}


<?php

namespace Ekyna\Component\Commerce\Payment\EventListener;

use Ekyna\Component\Commerce\Cart\Model\CartInterface;
use Ekyna\Component\Commerce\Cart\Model\CartStates;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Transformer\SaleTransformerInterface;
use Ekyna\Component\Commerce\Exception\RuntimeException;
use Ekyna\Component\Commerce\Order\Model\OrderInterface;
use Ekyna\Component\Commerce\Order\Repository\OrderRepositoryInterface;
use Ekyna\Component\Commerce\Payment\Event\PaymentEvent;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Ekyna\Component\Commerce\Quote\Model\QuoteInterface;
use Ekyna\Component\Commerce\Quote\Model\QuoteStates;
use Payum\Core\Payum;
use Payum\Core\Security\TokenInterface;
use Payum\Core\Storage\IdentityInterface;

/**
 * Class PaymentDoneEventSubscriber
 * @package Ekyna\Component\Commerce\Payment\EventListener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class PaymentDoneEventSubscriber
{
    /**
     * @var SaleTransformerInterface
     */
    private $saleTransformer;

    /**
     * @var OrderRepositoryInterface
     */
    private $orderRepository;

    /**
     * @var Payum
     */
    private $payum;

    /**
     * Constructor.
     *
     * @param SaleTransformerInterface $saleTransformer
     * @param OrderRepositoryInterface $orderRepository
     * @param Payum                    $payum
     */
    public function __construct(
        SaleTransformerInterface $saleTransformer,
        OrderRepositoryInterface $orderRepository,
        Payum $payum
    ) {
        $this->saleTransformer = $saleTransformer;
        $this->orderRepository = $orderRepository;
        $this->payum = $payum;
    }

    /**
     * Payment status event handler.
     *
     * Transforms an accepted cart/quote to an order.
     *
     * @param PaymentEvent $event
     */
    public function onStatus(PaymentEvent $event)
    {
        $payment = $event->getPayment();

        $sale = $payment->getSale();

        if ($sale instanceof OrderInterface) {
            return;
        }

        if ($sale instanceof CartInterface && $sale->getState() !== CartStates::STATE_ACCEPTED) {
            return;
        }

        if ($sale instanceof QuoteInterface && $sale->getState() !== QuoteStates::STATE_ACCEPTED) {
            return;
        }

        // Store payment tokens
        $tokens = $this->findPaymentTokens($payment);

        // Transform sale to order
        if (null === $order = $this->transform($sale)) {
            return;
        }

        // Find order's transformed payment
        $newPayment = null;
        foreach ($order->getPayments() as $p) {
            if ($p->getNumber() === $payment->getNumber()) {
                $newPayment = $p;
                break;
            }
        }
        if (null === $newPayment) {
            throw new RuntimeException("Failed to find the transformed payment.");
        }

        // Convert tokens
        $this->convertTokens($this->getPaymentIdentity($newPayment), $tokens);

        // Set event new payment
        $event->setPayment($newPayment);
    }

    /**
     * Find the given payment's security tokens.
     *
     * @param PaymentInterface $payment
     *
     * @return TokenInterface[]
     */
    private function findPaymentTokens(PaymentInterface $payment)
    {
        $identity = $this->getPaymentIdentity($payment);

        /** @var TokenInterface[] $tokens */
        $tokens = $this->payum->getTokenStorage()->findBy([
            'details' => $identity,
        ]);

        return $tokens;
    }

    /**
     * Transforms the given cart or quote to an order.
     *
     * @param SaleInterface $sale
     *
     * @return OrderInterface|null
     */
    private function transform(SaleInterface $sale)
    {
        $order = $this->newOrder();

        // Initialize transformation
        $this->saleTransformer->initialize($sale, $order);

        // Transform
        if (null === $event = $this->saleTransformer->transform()) {
            // Success
            return $order;
        }

        return null;
    }

    /**
     * @param IdentityInterface $identity New payment's identity
     * @param TokenInterface[]  $tokens   The original payment's tokens
     */
    private function convertTokens(IdentityInterface $identity, array $tokens)
    {
        if (empty($tokens)) {
            return;
        }

        // Update tokens identity
        $storage = $this->payum->getTokenStorage();
        foreach ($tokens as $t) {
            $t->setDetails($identity);
            $storage->update($t);
        }
    }

    /**
     * Returns the payment identity.
     *
     * @param PaymentInterface $payment
     *
     * @return \Payum\Core\Storage\IdentityInterface
     */
    private function getPaymentIdentity(PaymentInterface $payment)
    {
        return $this->payum->getStorage($payment)->identify($payment);
    }

    /**
     * Returns a new order.
     *
     * @return \Ekyna\Bundle\CommerceBundle\Model\OrderInterface
     */
    private function newOrder()
    {
        /** @noinspection PhpIncompatibleReturnTypeInspection */
        return $this->orderRepository->createNew();
    }
}


<?php

namespace SnooPHP\Git;

use SnooPHP\Http\Request;
use SnooPHP\Http\Response;

/**
 * A webhook designed to work with github webhook system
 *
 * @author sneppy
 */
class GitHubWebhook extends Webhook
{
	/**
	 * Handle a webhook request coming from github
	 *
	 * @param Request $request webhook request
	 *
	 * @return Response
	 */
	public static function handle(Request $request)
	{
		global $webhookConfig;
		if (!$webhookConfig)
		{
			error_log("error: missing webhook config!");
			Response::abort(500, [
				"status"		=> "ERROR",
				"description"	=> "webhook misconfiguration"
			]);
		}

		$payload = from_json($request->input("payload", "null"));
		if (!$payload) Response::abort(400, [
			"status"		=> "ERROR",
			"description"	=> "payload not found"
		]);

		// Allow only from known addresses
		$allowed	= false;
		$ip			= $request->header("Remote Address") ?: $request->header("X-Client-Ip") ?: $request->header("X-Forwarded-For");
		if ($ip && $webhookConfig["strong_ip_validation"])
		{
			foreach ($webhookConfig["whitelist"] as $test) $allowed = $allowed || validate_ip($ip, $test);
			if (!$allowed) Response::abort(403, [
				"status"		=> "ERROR",
				"description"	=> "ip not whitelisted"
			]);
		}

		// Check repository
		if ($payload->repository->id !== $webhookConfig["rep_id"]) Response::abort(400, [
			"status"		=> "ERROR",
			"description"	=> "invalid repository id"
		]);

		// Run script
		if (file_exists($webhookConfig["script"]))
		{
			$script	= $webhookConfig["script"]." {$request->header("X-GitHub-Delivery")} ".($webhookConfig["branch"] ?? "master");
			$output	= `$script`;
			return Response::json([
				"status"		=> "OK",
				"description"	=> "webhook deployed",
				"output"		=> $output
			]);
		}
		else
			Response::abort(500, [
				"status"		=> "ERROR",
				"description"	=> "webhook script not found"
			]);
	}
}


<?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\Traits;

trait SolsticeEquinoxTrait {

  /**
   * Finds the date of the time of the March Equinox for a given year
   *
   * @param float $year The year
   * @param bool  $vsop If true calculates the result using the VSOP model; If
   *                    false approximates the result.
   *
   * @return float The JD and time of the March Equinox in TD
   */
  protected static function equinoxMarch($year, $vsop = true) {
    $month = 3;

    if ($vsop)
      return static::accurate($year, static::meanTerms($month, $year), $month);
    else
      return static::approx($year, static::meanTerms($month, $year));
  }

  /**
   * Finds the date of the time of the September Equinox for a given year
   *
   * @param float $year The year
   * @param bool  $vsop If true calculates the result using the VSOP model; If
   *                    false approximates the result.
   *
   * @return float The JD and time of the September Equinox in TD
   */
  protected static function equinoxSeptember($year, $vsop = true) {
    $month = 9;

    if ($vsop)
      return static::accurate($year, static::meanTerms($month, $year), $month);
    else
      return static::approx($year, static::meanTerms($month, $year));
  }

  /**
   * Finds the date of the time of the June Solstice for a given year
   *
   * @param float $year The year
   * @param bool  $vsop If true calculates the result using the VSOP model; If
   *                    false, approximates the result
   *
   * @return float The JD time of the June Solstice in TD
   */
  protected static function solsticeJune($year, $vsop = true) {
    $month = 6;

    if ($vsop)
      return static::accurate($year, static::meanTerms($month, $year), $month);
    else
      return static::approx($year, static::meanTerms($month, $year));
  }

  /**
   * Finds the date of the time of the December Solstice for a given year
   *
   * @param float $year The year
   * @param bool  $vsop If true calculates the result using the VSOP model; If
   *                    false, approximates the result
   *
   * @return float The JD and time of the December Solstice in TD
   */
  protected static function solsticeDecember($year, $vsop = true) {
    $month = 12;

    if ($vsop)
      return static::accurate($year, static::meanTerms($month, $year), $month);
    else
      return static::approx($year, static::meanTerms($month, $year));
  }

  // // // Private

  /**
   * Finds the approximate time of a solstice or equinox in TD
   *
   * @param int   $year  The year to find a solstice or equinox for
   * @param array $terms An array of the mean terms for the desired equinox or
   *                     solstice
   *
   * @return float The JD of the solstice or equinox in TD
   */
  private static function approx($year, array $terms) {
    // Find appropriate year term and calculate approximate JDE
    $Y    = $year < 1000 ? (int)$year / 1000 : ((int)$year - 2000) / 1000;
    $jde0 = static::Horner($Y, $terms);

    // Calculate T and other required values
    $T  = ($jde0 - 2451545.0 ) / 36525;
    $W  = deg2rad(35999.373 * $T - 2.47);
    $Δλ = 1 + 0.0334 * cos($W) + 0.0007 * cos(2 * $W);

    // Sum the periodic terms for the solstice or equinox
    $S = 0;
    foreach (static::periodicTerms() as $term)
      $S += $term[0] * cos(deg2rad($term[1]) + deg2rad($term[2]) * $T);

    // Calculate the time of the solstice or equinox
    $jde = $jde0 + ((0.00001 * $S) / $Δλ);
    return $jde;
  }

  /**
   * Finds the accurate time of a solstice or equinox using the complete VSOP87
   * theory
   *
   * @param int   $year  The year to find a solstice or equinox for
   * @param array $terms An array of the mean terms for the desired equinox or
   *                     solstice
   * @param int   $month The month of the equinox or solstice to find
   *
   * @return float The JD of the solstice or equinox in TD
   */
  private static function accurate($year, array $terms, $month) {
    $Y = $year < 1000 ? (int)$year / 1000 : ((int)$year - 2000) / 1000;
    $q = intval($month / 3) - 1;

    $jde0 = static::Horner($Y, $terms);
    for ($i = 0; $i < 100; $i++) {
      // TODO:: use vsop 87, but use IAU apparent stuff?
      $λ = Solar::ApparentEclVSOP87(AstroDate::fromJD($jde0))->λ->rad;
      $Δ = 58 * sin(deg2rad($q * 90) - $λ);
      $jde0 += $Δ;

      if (abs($Δ) < 5e-7)
        break;
    }

    return $jde0;
  }

  /**
   * Gets the mean equinox/solstice terms for a given month and year grouping
   *
   * @param int $month The month of the equinox or solstice
   * @param int $year  The start maximum year of the terms, options are
   *                   1000 = -1000 to 1000 and 3000 = 1000 to 3000
   *
   * @return array The mean equinox/solstice terms
   */
  private static function meanTerms($month, $year) {
    $terms = [
        3  => [
            1000 => [1721139.29189, 365242.13740, .06134, .00111, -.00071],
            3000 => [2451623.80984, 365242.37404, .05169, -.00411, -.00057]
        ],
        6  => [
            1000 => [1721233.25401, 365241.72562, -.05232, .00907, .00025],
            3000 => [2451716.56767, 365241.62603, .00325, .00888, -.00030]
        ],
        9  => [
            1000 => [1721325.70455, 365242.49558, -.11677, -.00297, .00074],
            3000 => [2451810.21715, 365242.01767, -.11575, .00337, .00078]
        ],
        12 => [
            1000 => [1721414.39987, 365242.88257, -.00769, -.00933, -.00006],
            3000 => [2451900.05952, 365242.74049, -.06223, -.00823, .00032]
        ],
    ];

    $y = $year < 1000 ? 1000 : 3000;
    return $terms[$month][$y];
  }

  /**
   * Gets the periodic terms used in the calculation of equinoxes and solstices
   * @return array
   */
  private static function periodicTerms() {
    return [
        [485, 324.96, 1934.136],
        [203, 337.23, 32964.467],
        [199, 342.08, 20.186],
        [182, 27.85, 445267.112],
        [156, 73.14, 45036.886],
        [136, 171.52, 22518.443],
        [77, 222.54, 65928.934],
        [74, 296.72, 3034.906],
        [70, 243.58, 9037.513],
        [58, 119.81, 33718.147],
        [52, 297.17, 150.678],
        [50, 21.02, 2281.226],
        [45, 247.54, 29929.562],
        [44, 325.15, 31555.956],
        [29, 60.93, 4443.417],
        [18, 155.12, 67555.328],
        [17, 288.79, 4562.452],
        [16, 198.04, 62894.029],
        [14, 199.76, 31436.921],
        [12, 95.39, 14577.848],
        [12, 287.11, 31931.756],
        [12, 320.81, 34777.259],
        [9, 227.73, 1222.114],
        [8, 15.45, 16859.074],
    ];
  }

  /**
   * Evaluates a polynomial with coefficients c at x of which x is the constant
   * term by means of the Horner method
   *
   * @param float $x The constant term
   * @param array $c The coefficients of the polynomial
   *
   * @return float                    The value of the polynomial
   * @throws InvalidArgumentException Occurs if no coefficients are provided
   *
   * @see p.10-11, 'Avoiding powers'
   */
  private static function horner($x, $c) {
    if (count($c) == 0)
      throw new InvalidArgumentException('No coefficients were provided');

    $i = count($c) - 1;
    $y = $c[$i];
    while ($i > 0) {
      $i--;
      $y = $y * $x + $c[$i];
    }

    return $y;
  }

}


<?php

namespace PeskyCMF\Scaffold;

use PeskyCMF\Scaffold\DataGrid\DataGridConfig;
use PeskyCMF\Scaffold\Form\FormConfig;
use PeskyCMF\Scaffold\ItemDetails\ItemDetailsConfig;
use PeskyORM\ORM\Column;
use PeskyORM\ORM\Relation;
use Swayok\Html\Tag;

abstract class AbstractValueViewer {

    /** @var null|ScaffoldSectionConfig */
    protected $scaffoldSectionConfig = null;

    /** @var string|null */
    protected $name = null;

    /** @var string */
    protected $type = null;
    const TYPE_STRING = Column::TYPE_STRING;
    const TYPE_DATE = Column::TYPE_DATE;
    const TYPE_TIME = Column::TYPE_TIME;
    const TYPE_DATETIME = Column::TYPE_TIMESTAMP;
    const TYPE_BOOL = Column::TYPE_BOOL;
    const TYPE_TEXT = Column::TYPE_TEXT;
    const TYPE_MULTILINE = 'multiline'; //< for non-html multiline text
    const TYPE_IMAGE = 'image';
    const TYPE_JSON = Column::TYPE_JSON;
    const TYPE_JSONB = Column::TYPE_JSONB;
    const TYPE_LINK = 'link';
    /**
     * @var null|string
     */
    protected $label = null;
    /**
     * Position
     * @var null|int
     */
    protected $position = null;

    /**
     * @var null|\Closure
     */
    protected $valueConverter = null;
    const FORMAT_DATE = 'Y-m-d';
    const FORMAT_TIME = 'H:i:s';
    const FORMAT_DATETIME = 'Y-m-d H:i:s';
    /** @var bool */
    protected $isLinkedToDbColumn = true;
    /** @var Relation */
    protected $relation;
    /** @var string */
    protected $relationColumn;
    /** @var string|null */
    protected $nameForTranslation;
    /** @var string|null */
    protected $tableNameForRouteToRelatedRecord;

    /**
     * @return $this
     */
    static public function create() {
        $classname = get_called_class();
        return new $classname();
    }

    /**
     * @return ScaffoldSectionConfig|DataGridConfig|ItemDetailsConfig|FormConfig
     */
    public function getScaffoldSectionConfig() {
        return $this->scaffoldSectionConfig;
    }

    /**
     * @param ScaffoldSectionConfig|null $scaffoldSectionConfig
     * @return $this
     */
    public function setScaffoldSectionConfig($scaffoldSectionConfig) {
        $this->scaffoldSectionConfig = $scaffoldSectionConfig;
        return $this;
    }

    /**
     * @return Column
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     * @throws ValueViewerConfigException
     */
    public function getTableColumn() {
        if ($this->relation) {
            return $this->relation->getForeignTable()->getTableStructure()->getColumn($this->relationColumn);
        } else {
            list($columnName, ) = static::splitComplexViewerName($this->getName());
            return $this->getScaffoldSectionConfig()->getTable()->getTableStructure()->getColumn($columnName);
        }
    }

    /**
     * @param Relation $relation
     * @param string $columnName
     * @return $this
     */
    public function setRelation(Relation $relation, $columnName) {
        $this->relation = $relation;
        $this->relationColumn = $columnName;
        return $this;
    }

    /**
     * @return Relation
     */
    public function getRelation() {
        return $this->relation;
    }

    /**
     * @return bool
     */
    public function hasRelation() {
        return !empty($this->relation);
    }

    /**
     * @return string
     */
    public function getRelationColumn() {
        return $this->relationColumn;
    }

    /**
     * Used only for value cells that make  tags to generate valid urls to related records
     * @param string $tableName
     * @return $this
     */
    public function setTableNameForRouteToRelatedRecord($tableName) {
        $this->tableNameForRouteToRelatedRecord = $tableName;
        return $this;
    }

    /**
     * @return null|string
     */
    public function getTableNameForRouteToRelatedRecord() {
        return $this->tableNameForRouteToRelatedRecord;
    }

    /**
     * @param bool $isDbColumn
     * @return $this
     */
    public function setIsLinkedToDbColumn($isDbColumn) {
        $this->isLinkedToDbColumn = $isDbColumn;
        return $this;
    }

    /**
     * @return bool
     */
    public function isLinkedToDbColumn() {
        return $this->isLinkedToDbColumn;
    }

    /**
     * @return null|string
     * @throws ValueViewerConfigException
     */
    public function getName() {
        if (empty($this->name)) {
            throw new ValueViewerConfigException($this, 'Value viewer name not provided');
        }
        return $this->name;
    }

    /**
     * Check if name is something like "column_name:key_name"
     * @param string $name
     * @return bool
     */
    final static public function isComplexViewerName($name) {
        return (bool)preg_match('%^[^:]+?:[^:]+?$%', $name);
    }

    /**
     * @param string $name - something like "column_name:key_name"
     * @return array - 0 - column name; 1 = key name or null
     */
    final static public function splitComplexViewerName($name) {
        $parts = explode(':', $name, 2);
        if (count($parts) === 1) {
            $parts[1] = null;
        }
        return $parts;
    }

    /**
     * @param null|string $name
     * @return $this
     */
    public function setName($name) {
        $this->name = $name;
        return $this;
    }

    public function getNameForTranslation() {
        if ($this->nameForTranslation === null) {
            $this->nameForTranslation = $this->getName();
        }
        return $this->nameForTranslation;
    }

    /**
     * @param string $name
     * @return $this
     */
    public function setNameForTranslation($name) {
        $this->nameForTranslation = $name;
        return $this;
    }

    /**
     * @return string
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     */
    public function getType() {
        if (empty($this->type)) {
            if ($this->isLinkedToDbColumn() && !static::isComplexViewerName($this->getName())) {
                $this->setType($this->getTableColumn()->getType());
            } else {
                $this->setType(static::TYPE_STRING);
            }
        }
        return $this->type;
    }

    /**
     * @param string $type
     * @return $this
     */
    public function setType($type) {
        $this->type = $type;
        return $this;
    }

    /**
     * @return string
     * @throws \UnexpectedValueException
     */
    public function getLabel() {
        if ($this->label === null) {
            $this->label = $this->getScaffoldSectionConfig()->translate($this);
            if (!is_string($this->label)) {
                throw new \UnexpectedValueException(
                    "Label for value viewer '{$this->getName()}' must be a string. " . ucfirst(gettype($this->label)) . 'received'
                );
            }
        }
        return $this->label;
    }

    /**
     * @param null|string $label
     * @return $this
     */
    public function setLabel($label) {
        $this->label = $label;
        return $this;
    }

    /**
     * @return int|null
     */
    public function getPosition() {
        return $this->position;
    }

    /**
     * @param int|null $position
     * @return $this
     */
    public function setPosition($position) {
        $this->position = $position;
        return $this;
    }

    /**
     * @return boolean
     */
    public function hasValueConverter() {
        return !empty($this->valueConverter);
    }

    /**
     * @return \Closure|null
     */
    public function getValueConverter() {
        return $this->valueConverter;
    }

    /**
     * @param \Closure $valueConverter
     *      - when $this->isDbField() === true: function ($value, Column $columnConfig, array $record, AbstractValueViewer $valueViewer) { return 'value' }
     *      - when $this->isDbField() === false: function (array $record, AbstractValueViewer $valueViewer, ScaffoldSectionConfig $scaffoldSectionConfig) { return 'value' }
     * @return $this
     */
    public function setValueConverter(\Closure $valueConverter) {
        $this->valueConverter = $valueConverter;
        return $this;
    }

    /**
     * @param mixed $value
     * @param array $record
     * @param bool $ignoreValueConverter
     * @return mixed
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     * @throws ValueViewerConfigException
     */
    public function convertValue($value, array $record, $ignoreValueConverter = false) {
        $valueConverter = !$ignoreValueConverter ? $this->getValueConverter() : null;
        if (!empty($valueConverter)) {
            if ($this->isLinkedToDbColumn()) {
                $value = call_user_func($valueConverter, $value, $this->getTableColumn(), $record, $this);
            } else {
                $value = call_user_func($valueConverter, $record, $this, $this->getScaffoldSectionConfig());
            }
        } else if (!empty($value) || is_bool($value)) {
            if ($this->getType() === static::TYPE_LINK && $this->isLinkedToDbColumn()) {
                return $this->buildLinkToExternalRecord($this->getTableColumn(), $record);
            } else {
                return $this->doDefaultValueConversionByType($value, $this->type, $record);
            }
        }
        return $value;
    }

    /**
     * Default value converter by value type
     * @param mixed $value
     * @param string $type - one of static::TYPE_*
     * @param array $record
     * @return mixed
     */
    public function doDefaultValueConversionByType($value, $type, array $record) {
        switch ($type) {
            case static::TYPE_DATETIME:
                return date(static::FORMAT_DATETIME, is_numeric($value) ? $value : strtotime($value));
            case static::TYPE_DATE:
                return date(static::FORMAT_DATE, is_numeric($value) ? $value : strtotime($value));
            case static::TYPE_TIME:
                return date(static::FORMAT_TIME, is_numeric($value) ? $value : strtotime($value));
            case static::TYPE_MULTILINE:
                return '
' . $value . '
';
            case static::TYPE_JSON:
            case static::TYPE_JSONB:
                if (!is_array($value) && $value !== null) {
                    if (is_string($value) || is_numeric($value) || is_bool($value)) {
                        $value = json_decode($value, true);
                        if ($value === null && strtolower($value) !== 'null') {
                            $value = 'Failed to decode JSON: ' . print_r($value, true);
                        }
                    } else {
                        $value = 'Invalid value for JSON: ' . print_r($value, true);
                    }
                }
                return '
'
                        . htmlentities(stripslashes(json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)))
                    . '
';
                break;
        }
        return $value;
    }

    public function buildLinkToExternalRecord(Column $columnConfig, array $record, $linkLabel = null) {
        if (empty($record[$columnConfig->getName()])) {
            return '-';
        }
        $relationConfig = null;
        $relationAlias = null;
        $relationColumn = null;
        $relationData = [];
        if ($this->hasRelation()) {
            $relationConfig = $this->getRelation();
            $relationColumn = $relationConfig->getDisplayColumnName();
            $relationData = array_get($record, $relationConfig->getName(), $record);
        } else {
            foreach ($columnConfig->getRelations() as $relation) {
                if (in_array($relation->getType(), [Relation::BELONGS_TO, Relation::HAS_ONE], true)) {
                    $relationConfig = $relation;
                    $relationColumn = $relationConfig->getDisplayColumnName();
                    $relationData = array_get($record, $relationConfig->getName());
                    break;
                }
            }
        }
        if (empty($relationConfig)) {
            throw new ValueViewerConfigException($this, "Column [{$columnConfig->getName()}] has no fitting relation");
        }
        $relationPkColumn = $relationConfig->getForeignTable()->getPkColumnName();
        if (empty($relationData) || empty($relationData[$relationPkColumn])) {
            return $this->getScaffoldSectionConfig()->translateGeneral('field.no_relation');
        } else {
            if (empty($linkLabel)) {
                if ($relationColumn instanceof \Closure) {
                    $linkLabel = $relationColumn($relationData);
                } else {
                    if (empty($relationData[$relationColumn])) {
                        if ($relationConfig->getForeignTable()->getTableStructure()->hasColumn($relationColumn)) {
                            $linkLabel = $relationConfig
                                ->getForeignTable()
                                ->newRecord()
                                    ->enableTrustModeForDbData()
                                    ->fromData($relationData, true, false)
                                    ->getValue($relationColumn);
                        } else {
                            $relationColumn = $relationPkColumn;
                        }
                    }
                    /** @noinspection NotOptimalIfConditionsInspection */
                    if (empty($linkLabel)) {
                        $linkLabel = $relationData[$relationColumn];
                    }
                }
            }
            return Tag::a($linkLabel)
                ->setHref(routeToCmfItemDetails(
                    $this->getTableNameForRouteToRelatedRecord() ?: $relationConfig->getForeignTable()->getName(),
                    $relationData[$relationPkColumn]
                ))
                ->build();
        }
    }

}


<?php

/*
 * This file is part of NotifyMe.
 *
 * (c) Alt Three Services Limited
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace NotifyMeHQ\Laravel;

use Illuminate\Support\ServiceProvider;
use NotifyMeHQ\Contracts\FactoryInterface;
use NotifyMeHQ\Contracts\ManagerInterface;
use NotifyMeHQ\Factory\NotifyMeFactory;

/**
 * This is the notifyme service provider class.
 *
 * @author Graham Campbell <graham@alt-three.com>
 */
class NotifyMeServiceProvider extends ServiceProvider
{
    /**
     * Boot the service provider.
     *
     * @return void
     */
    public function boot()
    {
        $this->setupConfig();
    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerFactory();
        $this->registerManager();
    }

    /**
     * Setup the config.
     *
     * @return void
     */
    protected function setupConfig()
    {
        $source = realpath(__DIR__.'/../config/notifyme.php');

        if (class_exists('Illuminate\Foundation\Application', false)) {
            $this->publishes([$source => config_path('notifyme.php')]);
        }

        $this->mergeConfigFrom($source, 'notifyme');
    }

    /**
     * Register the factory class.
     *
     * @return void
     */
    protected function registerFactory()
    {
        $this->app->singleton('notifyme.factory', function () {
            return new NotifyMeFactory();
        });

        $this->app->alias('notifyme.factory', NotifyMeFactory::class);
        $this->app->alias('notifyme.factory', FactoryInterface::class);
    }

    /**
     * Register the manager class.
     *
     * @return void
     */
    protected function registerManager()
    {
        $this->app->singleton('notifyme', function ($app) {
            $config = $app['config'];
            $factory = $app['notifyme.factory'];

            return new NotifyMeManager($config, $factory);
        });

        $this->app->alias('notifyme', NotifyMeManager::class);
        $this->app->alias('notifyme', ManagerInterface::class);
    }

    /**
     * Get the services provided by the provider.
     *
     * @return string[]
     */
    public function provides()
    {
        return [
            'notifyme.factory',
            'notifyme',
        ];
    }
}


<?php

namespace Ekyna\Component\Commerce\Invoice\EventListener;

use Ekyna\Component\Commerce\Common\Generator\NumberGeneratorInterface;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Customer\Updater\CustomerUpdaterInterface;
use Ekyna\Component\Commerce\Document\Builder\DocumentBuilderInterface;
use Ekyna\Component\Commerce\Document\Calculator\DocumentCalculatorInterface;
use Ekyna\Component\Commerce\Exception;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceSubjectInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceTypes;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

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

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

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

    /**
     * @var DocumentBuilderInterface
     */
    protected $invoiceBuilder;

    /**
     * @var DocumentCalculatorInterface
     */
    protected $invoiceCalculator;

    /**
     * @var CustomerUpdaterInterface
     */
    protected $customerUpdater;

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

    /**
     * Sets the invoice number generator.
     *
     * @param NumberGeneratorInterface $generator
     */
    public function setInvoiceNumberGenerator(NumberGeneratorInterface $generator)
    {
        $this->invoiceNumberGenerator = $generator;
    }

    /**
     * Sets the credit number generator.
     *
     * @param NumberGeneratorInterface $generator
     */
    public function setCreditNumberGenerator(NumberGeneratorInterface $generator)
    {
        $this->creditNumberGenerator = $generator;
    }

    /**
     * Sets the invoice updater.
     *
     * @param DocumentBuilderInterface $invoiceBuilder
     */
    public function setInvoiceBuilder(DocumentBuilderInterface $invoiceBuilder)
    {
        $this->invoiceBuilder = $invoiceBuilder;
    }

    /**
     * Sets the invoice calculator.
     *
     * @param DocumentCalculatorInterface $invoiceCalculator
     */
    public function setInvoiceCalculator(DocumentCalculatorInterface $invoiceCalculator)
    {
        $this->invoiceCalculator = $invoiceCalculator;
    }

    /**
     * Sets the customerUpdater.
     *
     * @param CustomerUpdaterInterface $customerUpdater
     */
    public function setCustomerUpdater(CustomerUpdaterInterface $customerUpdater)
    {
        $this->customerUpdater = $customerUpdater;
    }

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

        // Generate number and key
        $changed = $this->generateNumber($invoice);

        // Updates the invoice data
        $changed |= $this->invoiceBuilder->update($invoice);

        // Updates the totals
        $changed |= $this->updateTotals($invoice);

        if ($changed) {
            $this->persistenceHelper->persistAndRecompute($invoice, false);
        }

        $sale = $this->getSaleFromInvoice($invoice);
        if ($sale instanceof InvoiceSubjectInterface) {
            $sale->addInvoice($invoice); // TODO wtf ?
        }

        $this->scheduleSaleContentChangeEvent($sale);
    }

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

        $this->preventForbiddenChange($invoice);

        // Generate number and key
        $changed = $this->generateNumber($invoice);

        // Updates the invoice data
        $changed |= $this->invoiceBuilder->update($invoice);

        // Updates the totals
        $changed |= $this->updateTotals($invoice);

        if ($this->persistenceHelper->isChanged($invoice, 'paymentMethod')) {
            $this->updateCustomerBalance($invoice);
        }

        if ($changed) {
            $this->persistenceHelper->persistAndRecompute($invoice, false);

            $this->scheduleSaleContentChangeEvent($this->getSaleFromInvoice($invoice));
        }
    }

    /**
     * Delete event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onDelete(ResourceEventInterface $event)
    {
        $invoice = $this->getInvoiceFromEvent($event);

        $this->updateCustomerBalance($invoice);

        $sale = $this->getSaleFromInvoice($invoice);

        $sale->removeInvoice($invoice);

        $this->scheduleSaleContentChangeEvent($sale);
    }

    /**
     * Content change event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onContentChange(ResourceEventInterface $event)
    {
        $invoice = $this->getInvoiceFromEvent($event);

        if (!$this->persistenceHelper->isScheduledForRemove($invoice)) {
            if ($this->updateTotals($invoice)) {
                $this->persistenceHelper->persistAndRecompute($invoice, false);
            }

            $this->updateCustomerBalance($invoice);
        }

        $sale = $this->getSaleFromInvoice($invoice);

        $this->scheduleSaleContentChangeEvent($sale);
    }

    /**
     * Pre update event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onPreUpdate(ResourceEventInterface $event)
    {
        $invoice = $this->getInvoiceFromEvent($event);

        /*if (null !== $invoice->getShipment()) {
            throw new Exception\IllegalOperationException(
                "Invoice (or credit) associated with a shipment (or return) can't be modified."
            );
        }*/

        // Pre load sale's invoices collection
        /** @var InvoiceSubjectInterface $sale */
        $sale = $invoice->getSale();
        $sale->getInvoices()->toArray();
    }

    /**
     * Pre delete event handler.
     *
     * @param ResourceEventInterface $event
     */
    public function onPreDelete(ResourceEventInterface $event)
    {
        $invoice = $this->getInvoiceFromEvent($event);

        // Pre load sale's invoices collection
        /** @var InvoiceSubjectInterface $sale */
        $sale = $invoice->getSale();
        $sale->getInvoices()->toArray();
    }

    /**
     * Updates the invoice totals.
     *
     * @param InvoiceInterface $invoice
     *
     * @return bool
     */
    protected function updateTotals(InvoiceInterface $invoice)
    {
        $changed = $this->invoiceCalculator->calculate($invoice);

        if ($changed) {
            foreach ($invoice->getLines() as $line) {
                $this->persistenceHelper->persistAndRecompute($line, false);
            }
        }

        return $changed;
    }

    /**
     * Updates the customer balance
     *
     * @param InvoiceInterface $invoice
     */
    protected function updateCustomerBalance(InvoiceInterface $invoice)
    {
        // Abort if not credit
        if (!InvoiceTypes::isCredit($invoice)) {
            return;
        }

        $sale = $this->getSaleFromInvoice($invoice);

        // Abort if no customer
        if (null === $customer = $sale->getCustomer()) {
            return;
        }

        $methodCs = $this->persistenceHelper->getChangeSet($invoice, 'paymentMethod');
        $amountCs = $this->persistenceHelper->getChangeSet($invoice, 'grandTotal');

        // Debit grand total if invoice is removed
        // TODO Multiple call will credit too much !
        if ($this->persistenceHelper->isScheduledForRemove($invoice)) {
            $method = empty($methodCs) ? $invoice->getPaymentMethod() : $methodCs[0];
            $amount = empty($amountCs) ? $invoice->getGrandTotal(): $amountCs[0];

            if ($method && $method->isCredit() && 0 != Money::compare($amount, 0, $invoice->getCurrency())) {
                $this->customerUpdater->updateCreditBalance($customer, -$amount, true);
            }

            return;
        }

        // Abort if nothing has changed
        if (empty($methodCs) && empty($amountCs)) {
            return;
        }

        // Debit old method customer balance
        /** @var \Ekyna\Component\Commerce\Payment\Model\PaymentMethodInterface $method */
        if (!empty($methodCs) && null !== $method = $methodCs[0]) {
            $amount = empty($amountCs) ? $invoice->getGrandTotal(): $amountCs[0];

            if ($method->isCredit() && 0 != Money::compare($amount, 0, $invoice->getCurrency())) {
                $this->customerUpdater->updateCreditBalance($customer, -$amount, true);
            }
        }

        // Credit new method customer balance
        if (empty($methodCs)) {
            $method = $invoice->getPaymentMethod();
            $amount = empty($amountCs) ? $invoice->getGrandTotal(): $amountCs[1] - $amountCs[0];
        } else {
            /** @var \Ekyna\Component\Commerce\Payment\Model\PaymentMethodInterface $method */
            $method = $methodCs[1];
            $amount = empty($amountCs) ? $invoice->getGrandTotal(): $amountCs[1];
        }
        if ($method && $method->isCredit() && 0 != Money::compare($amount, 0, $invoice->getCurrency())) {
            $this->customerUpdater->updateCreditBalance($customer, $amount, true);
        }
    }

    /**
     * Generates the number.
     *
     * @param InvoiceInterface $invoice
     *
     * @return bool Whether the invoice number has been generated or not.
     */
    protected function generateNumber(InvoiceInterface $invoice)
    {
        if (0 == strlen($invoice->getNumber())) {
            if (InvoiceTypes::isInvoice($invoice)) {
                $this->invoiceNumberGenerator->generate($invoice);
            } elseif (InvoiceTypes::isCredit($invoice)) {
                $this->creditNumberGenerator->generate($invoice);
            } else {
                throw new Exception\InvalidArgumentException("Unexpected invoice type.");
            }

            return true;
        }

        return false;
    }

    /**
     * Prevents some of the invoice's fields to change.
     *
     * @param InvoiceInterface $invoice
     *
     * @throws Exception\IllegalOperationException
     */
    protected function preventForbiddenChange(InvoiceInterface $invoice)
    {
        if ($this->persistenceHelper->isChanged($invoice, 'type')) {
            list($old, $new) = $this->persistenceHelper->getChangeSet($invoice, 'type');
            if ($old != $new) {
                throw new Exception\IllegalOperationException(
                    "Changing the invoice type is not yet supported."
                );
            }
        }
    }

    /**
     * Returns the invoice's sale.
     *
     * @param InvoiceInterface $invoice
     *
     * @return SaleInterface|InvoiceSubjectInterface
     */
    protected function getSaleFromInvoice(InvoiceInterface $invoice)
    {
        if (null === $sale = $invoice->getSale()) {
            $cs = $this->persistenceHelper->getChangeSet($invoice, $this->getSalePropertyPath());
            if (!empty($cs)) {
                $sale = $cs[0];
            }
        }

        if (!$sale instanceof SaleInterface) {
            throw new Exception\RuntimeException("Failed to retrieve invoice's sale.");
        }

        return $sale;
    }

    /**
     * Dispatches the sale content change event.
     *
     * @param SaleInterface $sale
     */
    abstract protected function scheduleSaleContentChangeEvent(SaleInterface $sale);

    /**
     * Returns the invoice from the event.
     *
     * @param ResourceEventInterface $event
     *
     * @return InvoiceInterface
     * @throws Exception\InvalidArgumentException
     */
    abstract protected function getInvoiceFromEvent(ResourceEventInterface $event);

    /**
     * Returns the invoice's sale property path.
     *
     * @return string
     */
    abstract protected function getSalePropertyPath();
}