<?php declare(strict_types=1);

namespace Ekyna\Component\Commerce\Common\Calculator;

use Ekyna\Component\Commerce\Common\Context\ContextProviderInterface;
use Ekyna\Component\Commerce\Common\Currency\CurrencyConverterInterface;
use Ekyna\Component\Commerce\Common\Model;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Exception;

/**
 * Class AmountCalculator
 * @package Ekyna\Component\Commerce\Common\Calculator
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class AmountCalculator implements AmountCalculatorInterface
{
    /**
     * @var ContextProviderInterface
     */
    private $contextProvider;

    /**
     * @var CurrencyConverterInterface
     */
    private $converter;

    /**
     * @var bool
     */
    private $cache = true;

    /**
     * Constructor.
     *
     * @param ContextProviderInterface   $contextProvider
     * @param CurrencyConverterInterface $converter
     */
    public function __construct(ContextProviderInterface $contextProvider, CurrencyConverterInterface $converter)
    {
        $this->contextProvider = $contextProvider;
        $this->converter = $converter;
    }

    /**
     * @inheritdoc
     */
    public function setCache(bool $cache): void
    {
        $this->cache = $cache;
    }

    /**
     * @inheritdoc
     */
    public function calculateSale(Model\SaleInterface $sale): Amount
    {
        $this->setCache(true);

        // Don't calculate twice
        if (null !== $result = $sale->getFinalResult()) {
            return $result;
        }

        // Items / Gross result
        $gross = $this->calculateSaleItems($sale);

        // Final result
        $final = Amount::createFinalFromGross($gross);

        // Discounts
        if ($sale->hasAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT)) {
            foreach ($sale->getAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT) as $adjustment) {
                $this->calculateSaleDiscount($adjustment, $gross, $final);
            }
        }

        // Shipment
        $this->calculateSaleShipment($sale, $final);

        // Round/finalize results.
        $gross->round();
        $final->finalize();

        // Store the results
        $sale->setGrossResult($gross);
        $sale->setFinalResult($final);

        return $final;
    }

    /**
     * @inheritdoc
     */
    public function calculateSaleItem(Model\SaleItemInterface $item, float $quantity = null): Amount
    {
        // TODO use packaging format on quantities

        if (null !== $quantity && 0 == $quantity) {
            throw new Exception\InvalidArgumentException("Custom quantity must be greater than zero.");
        }

        // Don't calculate twice
        if ($this->cache && null !== $result = $item->getResult()) {
            return $result;
        }

        $sale = $item->getSale();
        $currency = $sale->getCurrency()->getCode();
        $ati = $sale->isAtiDisplayMode();

        $taxGroup = $item->getTaxGroup();

        // Round unit price only for 'net' calculation
        $unit = $this->convert($sale, (float)$item->getNetPrice(), $currency, !$ati);

        // Add private items unit prices
        foreach ($item->getChildren() as $child) {
            $childResult = $this->calculateSaleItem($child,
                null !== $quantity ? $quantity * $child->getQuantity() : null);

            if ($child->isPrivate()) {
                if ($taxGroup !== $child->getTaxGroup()) {
                    throw new Exception\LogicException("Private item must have the same tax group as its parent.");
                }
                if ($child->hasAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT)) {
                    throw new Exception\LogicException("Private items can't have discount adjustment.");
                }

                $unit += $ati
                    ? $childResult->getUnit() * $child->getQuantity()
                    : Money::round($childResult->getUnit(), $currency) * $child->getQuantity();
            }
        }

        if ($item->isPrivate()) {
            // Private case : we just need unit amount
            $gross = $unit * $item->getTotalQuantity();
            $result = new Amount($currency, $unit, $gross, 0, $gross);
        } elseif ($item->getSale()->isSample()) {
            // Sample sale case : zero amounts
            $result = new Amount($currency);
        } else {
            // Regular case
            $discounts = $taxes = [];
            $discount = $tax = 0;

            // Gross price
            $gross = $unit * (null !== $quantity ? $quantity : $item->getTotalQuantity());

            $parent = $item->getParent();
            $discountAdjustments = $item->getAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT)->toArray();

            // Items without subject inherit discounts from their non-compound parent
            if (empty($discountAdjustments) && !$item->hasSubjectIdentity() && null !== $parent && !$parent->isCompound()) {
                $discountAdjustments = $parent->getAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT)->toArray();
            }

            // Discount amount and result adjustments
            $discountBase = $gross;
            foreach ($discountAdjustments as $data) {
                $adjustment = $this->createPercentAdjustment($data, $discountBase, $currency);
                $discountBase -= $adjustment->getAmount();
                $discount += $adjustment->getAmount();
                $discounts[] = $adjustment;
            }

            // Base
            $base = $ati
                ? round($gross - $discount, 5)
                : Money::round($gross - $discount, $currency);

            // Tax amount and result adjustments
            $taxAdjustments = $item->getAdjustments(Model\AdjustmentTypes::TYPE_TAXATION)->toArray();
            foreach ($taxAdjustments as $data) {
                $adjustment = $this->createPercentAdjustment($data, $base, $currency);

                $tax += $adjustment->getAmount();
                $taxes[] = $adjustment;
            }

            // Total
            $total = Money::round($base + $tax, $currency);

            // Result
            $result = new Amount($currency, $unit, $gross, $discount, $base, $tax, $total, $discounts, $taxes);
        }

        if ($ati) {
            $result->round();
        }

        // Store the result
        $item->setResult($result);

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function calculateSaleDiscount(
        Model\SaleAdjustmentInterface $adjustment,
        Amount $gross,
        Amount $final
    ): Amount {
        // Don't calculate twice
        if ($this->cache && null !== $result = $adjustment->getResult()) {
            return $result;
        }

        $this->assertAdjustmentType($adjustment, Model\AdjustmentTypes::TYPE_DISCOUNT);

        /** @var Model\SaleInterface $sale */
        $sale = $adjustment->getAdjustable();
        $currency = $sale->getCurrency()->getCode();

        // Sample sale case
        if ($sale->isSample()) {
            $result = new Amount($currency);
            $adjustment->setResult($result);

            return $result;
        }
        $base = $gross->getBase();

        $result = new Amount($currency);

        $mode = $adjustment->getMode();
        if (Model\AdjustmentModes::MODE_PERCENT === $mode) {
            $rate = (float)$adjustment->getAmount();

            $result->addUnit(Money::round($base * $rate / 100, $currency));

            foreach ($gross->getTaxAdjustments() as $tax) {
                $amount = Money::round($tax->getAmount() * $rate / 100, $currency);
                $result->addTaxAdjustment(new Adjustment($tax->getName(), $amount, $tax->getRate()));
                $result->addTax($amount);
            }
        } elseif (Model\AdjustmentModes::MODE_FLAT === $mode) {
            $realBase = $this->getRealGrossBase($sale);
            $amount = $this->convert($sale, (float)$adjustment->getAmount(), $currency, false);
            if (1 === Money::compare($realBase, $base, $currency)) {
                $unit = Money::round($amount * $base / $realBase, $currency);
            } else {
                $unit = $amount;
            }
            $result->addUnit($unit);

            foreach ($gross->getTaxAdjustments() as $tax) {
                $amount = Money::round($unit * $tax->getAmount() / $base, $currency);
                $result->addTaxAdjustment(new Adjustment($tax->getName(), $amount, $tax->getRate()));
                $result->addTax($amount);
            }
        } else {
            throw new Exception\InvalidArgumentException("Unexpected adjustment mode '$mode'.");
        }

        $result->addGross($result->getUnit());
        $result->addBase($result->getUnit());
        $result->addTotal($result->getUnit() + $result->getTax());

        $adjustment->setResult($result);

        // Add to final result
        $final->addDiscount($result->getBase());
        $final->addBase(-$result->getBase());
        $final->addTotal(-$result->getBase());
        $final->addDiscountAdjustment(new Adjustment(
            (string)$adjustment->getDesignation(),
            $result->getBase(),
            $adjustment->getMode() === Model\AdjustmentModes::MODE_PERCENT ? (float)$adjustment->getAmount() : 0
        ));

        foreach ($result->getTaxAdjustments() as $a) {
            $final->addTaxAdjustment(new Adjustment($a->getName(), -$a->getAmount(), $a->getRate()));
            $final->addTax(-$a->getAmount());
            $final->addTotal(-$a->getAmount());
        }

        if ($sale->isAtiDisplayMode()) {
            $result->round();
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function calculateSaleShipment(Model\SaleInterface $sale, Amount $final): ?Amount
    {
        // Don't calculate twice
        if ($this->cache && null !== $result = $sale->getShipmentResult()) {
            return $result;
        }

        $currency = $sale->getCurrency()->getCode();

        // Sample sale case
        if ($sale->isSample()) {
            $result = new Amount($currency);
            $sale->setShipmentResult($result);

            return $result;
        }

        // Abort if shipment cost is lower than or equals zero
        $base = $this->convert($sale, (float)$sale->getShipmentAmount(), $currency, false);
        $result = new Amount($currency, $base, $base, 0, $base, 0, $base);

        if (1 === Money::compare($base, 0, $currency)) {
            // Shipment taxation
            foreach ($sale->getAdjustments(Model\AdjustmentTypes::TYPE_TAXATION) as $data) {
                $adjustment = $this->createPercentAdjustment($data, $base, $currency);

                $result->addTaxAdjustment($adjustment);
                $result->addTax($adjustment->getAmount());
                $result->addTotal($adjustment->getAmount());
            }

            // Add to final result
            $final->addBase($result->getBase());
            $final->addTotal($result->getBase());

            foreach ($result->getTaxAdjustments() as $a) {
                $final->addTaxAdjustment($a);
                $final->addTax($a->getAmount());
                $final->addTotal($a->getAmount());
            }
        }

        if ($sale->isAtiDisplayMode()) {
            $result->round();
        }

        // Store shipment result
        $sale->setShipmentResult($result);

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function calculateSaleItems(Model\SaleInterface $sale): Amount
    {
        if ($this->cache && null !== $result = $sale->getGrossResult()) {
            return $result;
        }

        $result = new Amount($sale->getCurrency()->getCode());

        // Sum public
        foreach ($sale->getItems() as $item) {
            if ($item->isPrivate()) {
                throw new Exception\LogicException("Sale's root items can't be private.");
            }

            $this->calculateSaleItem($item);

            // Skip compound with only public children
            if (!($item->isCompound() && !$item->hasPrivateChildren())) {
                $result->merge($item->getResult());
            }

            $this->mergeItemsResults($item, $result);
        }

        // Set unit = gross
        $result->copyGrossToUnit();

        return $result;
    }

    /**
     * Merges the public children results recursively into the given result.
     *
     * @param Model\SaleItemInterface $item
     * @param Amount                  $result
     */
    protected function mergeItemsResults(Model\SaleItemInterface $item, Amount $result): void
    {
        // At this points items result are calculated and set.
        foreach ($item->getChildren() as $child) {
            if ($child->isPrivate()) {
                continue;
            }

            // Skip compound with only public children
            if (!($child->isCompound() && !$child->hasPrivateChildren())) {
                $result->merge($child->getResult());
            }

            if ($child->hasChildren()) {
                $this->mergeItemsResults($child, $result);
            }
        }
    }

    /**
     * Calculates the real sale gross base amount.
     *
     * @param Model\SaleInterface $sale
     *
     * @return float
     *
     * @throws Exception\LogicException
     */
    protected function getRealGrossBase(Model\SaleInterface $sale): float
    {
        // Store previous cache mode
        $cache = $this->cache;

        // Disable cache
        $this->cache = false;

        // Calculate real gross base
        $base = $this->calculateSaleItems($sale)->getBase();

        // Restore cache mode
        $this->cache = $cache;

        return $base;
    }

    /**
     * Creates a new result adjustment.
     *
     * @param Model\AdjustmentInterface $data
     * @param float                     $base
     * @param string                    $currency
     *
     * @return Adjustment
     */
    protected function createPercentAdjustment(
        Model\AdjustmentInterface $data,
        float $base,
        string $currency
    ): Adjustment {
        $this->assertAdjustmentMode($data, Model\AdjustmentModes::MODE_PERCENT);

        $rate = (float)$data->getAmount();

        if ($data->getType() === Model\AdjustmentTypes::TYPE_TAXATION) {
            // Calculate taxation as ATI - NET
            $amount = Money::round($base * (1 + $rate / 100), $currency) - Money::round($base, $currency);
        } else {
            $amount = Money::round($base * $rate / 100, $currency);
        }

        return new Adjustment((string)$data->getDesignation(), $amount, $rate);
    }

    /**
     * Asserts that the adjustment type is as expected.
     *
     * @param Model\AdjustmentInterface $adjustment
     * @param string                    $expected
     */
    protected function assertAdjustmentType(Model\AdjustmentInterface $adjustment, string $expected): void
    {
        if ($expected !== $type = $adjustment->getType()) {
            throw new Exception\InvalidArgumentException("Unexpected adjustment type '$type'.");
        }
    }

    /**
     * Asserts that the adjustment mode is as expected.
     *
     * @param Model\AdjustmentInterface $adjustment
     * @param string                    $expected
     */
    protected function assertAdjustmentMode(Model\AdjustmentInterface $adjustment, string $expected): void
    {
        if ($expected !== $mode = $adjustment->getMode()) {
            throw new Exception\InvalidArgumentException("Unexpected adjustment mode '$mode'.");
        }
    }

    /**
     * Converts the given amount.
     *
     * @param Model\SaleInterface $sale
     * @param float               $amount
     * @param string              $currency
     * @param bool                $round
     *
     * @return float
     * @throws \Exception
     */
    protected function convert(Model\SaleInterface $sale, float $amount, string $currency, bool $round)
    {
        if ($currency === $this->converter->getDefaultCurrency()) {
            return $round ? Money::round($amount, $currency) : $amount;
        }

        if (null !== $rate = $sale->getExchangeRate()) {
            return $this
                ->converter
                ->convertWithRate($amount, $rate, $currency, $round);
        }

        $date = $this->contextProvider->getContext($sale)->getDate();

        return $this
            ->converter
            ->convert($amount, $this->converter->getDefaultCurrency(), $currency, $date, $round);
    }
}


<?php

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

use Ekyna\Component\Commerce\Common\Model\SaleItemInterface;
use Ekyna\Component\Commerce\Exception\ValidationFailedException;
use Ekyna\Component\Commerce\Order\Model\OrderInterface;
use Ekyna\Component\Commerce\Stock\Helper\AvailabilityHelperInterface;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectInterface;
use Ekyna\Component\Commerce\Subject\SubjectHelperInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class SaleItemAvailabilityValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleItemAvailabilityValidator extends ConstraintValidator
{
    /**
     * @var SubjectHelperInterface
     */
    protected $subjectHelper;

    /**
     * @var AvailabilityHelperInterface
     */
    protected $availabilityHelper;

    /**
     * Constructor.
     *
     * @param SubjectHelperInterface      $subjectHelper
     * @param AvailabilityHelperInterface $availabilityHelper
     */
    public function __construct(
        SubjectHelperInterface $subjectHelper,
        AvailabilityHelperInterface $availabilityHelper
    ) {
        $this->subjectHelper = $subjectHelper;
        $this->availabilityHelper = $availabilityHelper;
    }

    /**
     * @inheritdoc
     */
    public function validate($item, Constraint $constraint)
    {
        if (null === $item) {
            return;
        }

        if (!$item instanceof SaleItemInterface) {
            throw new UnexpectedTypeException($item, SaleItemInterface::class);
        }
        if (!$constraint instanceof SaleItemAvailability) {
            throw new UnexpectedTypeException($constraint, SaleItemAvailability::class);
        }

        if (null !== $item->getParent()) {
            // This constraint should not be applied to children
            return;
        }

        $sale = $item->getSale();
        if ($sale instanceof OrderInterface) {
            // This constraint does not applies to orders
            return;
        }

        try {
            $this->validateItem($item);
        } catch (ValidationFailedException $e) {
            $this->context
                ->buildViolation($e->getMessage())
                ->addViolation();
        }
    }

    /**
     * Validates the sale item recursively.
     *
     * @param SaleItemInterface $item
     *
     * @throws ValidationFailedException
     */
    private function validateItem(SaleItemInterface $item)
    {
        foreach ($item->getChildren() as $child) {
            $this->validateItem($child);
        }

        if ($item->isCompound()) {
            return;
        }

        if (null === $subject = $this->subjectHelper->resolve($item, false)) {
            return;
        }

        if (!$subject instanceof StockSubjectInterface) {
            return;
        }

        $quantity = $item->getTotalQuantity();
        $availability = $this->availabilityHelper->getAvailability($subject, is_null($item->getParent()));

        if ($quantity < $availability->getMinimumQuantity()) {
            $message = $availability->getMinimumMessage();
        } elseif ($quantity > $availability->getMaximumQuantity()) {
            $message = $availability->getMaximumMessage();
        } else {
            return;
        }

        if (null !== $item->getParent()) {
            $message = $item->getDesignation() . ' : ' . $message;
        }

        throw new ValidationFailedException($message);
    }
}


<?php

namespace SnooPHP\Model;

use ReflectionClass;
use PDO;
use PDOException;

/**
 * A model represents an object of the application
 * Each model has a 1:1 connection with a related db table (e.g. User -> users)
 *
 * @author sneppy
 */
class Model
{
	/**
	 * @var string $idColumn id column of the table
	 */
	protected static $idColumn = "id";

	/**
	 * @var array $casts list of columns to cast and target type
	 */
	protected static $casts = [
		"id" => "int"
	];

	/**
	 * @var array $autos list of columns that have an auto value on update (i.e. 'modified_at' timestamp)
	 */
	protected static $autos = [
		"created_at",
		"updated_at"
	];

	/**
	 * @var array $casts list of columns to be converted from json
	 */
	protected static $jsons = [];

	/**
	 * Create a new model
	 *
	 * @param array $values associative array column => value
	 */
	public function __construct(array $values = [])
	{
		foreach ($values as $column => $val) $this->$column = $val;
	}

	/**
	 * Run query on this table and return model
	 *
	 * @param string	$queryString query string
	 * @param array		$queryParams query parameters
	 *
	 * @return Collection|bool return collection of results or false if fails
	 */
	public static function select($queryString = "", array $queryParams = [])
	{
		$tableName = static::tableName();

		$rows = Db::query("
			select $tableName.*
			from $tableName
			$queryString
		", $queryParams, static::getDbName());

		if ($rows === false)
			return false;
		if (empty($rows))
			return new Collection([]);

		// Populate models
		$collection = [];
		foreach ($rows as $row)
		{
			// Populate model
			$model = new static;
			foreach ($row as $column => $val) $model->$column = $model->decodeValue($val, $column);
			$collection[] = $model;
		}

		return new Collection($collection);
	}

	/**
	 * Find model and return it
	 *
	 * @param mixed		$id			id of the model
	 * @param string	$idColumn	optional id column different from default $idColumn
	 *
	 * @return static|bool single model or false if fails
	 */
	public static function find($id, $idColumn = null)
	{
		$tableName = static::tableName();
		$idColumn = $idColumn ?: static::$idColumn;

		$rows = Db::query("
			select *
			from $tableName
			where $idColumn = :id
		", ["id" => $id], static::getDbName());

		if ($rows === false)
			return false;
		if (empty($rows))
			return null;

		// Populate model
		$model = new static;
		foreach ($rows[0] as $col => $val) $model->$col = $model->decodeValue($val, $col);
		return $model;
	}

	/**
	 * One-to-one relationship
	 *
	 * @param string	$forClass	foreign model class name
	 * @param string	$forColumn	foreign column if it differs from className_id (ex. user_id)
	 *
	 * @return Model|bool foreign model or false if fails
	 */
	public function has($forClass, $forColumn = null)
	{
		$refTable = static::tableName();
		$forTable = $forClass::tableName();
		$refColumn = static::$idColumn;
		$forColumn = $forColumn ?: strtolower(static::modelName())."_id";

		$rows = Db::query("
			select F.*
			from $refTable as R, $forTable as F
			where R.$refColumn = F.$forColumn and R.$refColumn = :id
		", ["id" => $this->$refColumn], static::getDbName());

		if ($rows === false)
			return false;
		if (empty($rows))
			return null;

		// Populate model
		$forModel = new $forClass;
		foreach ($rows[0] as $col => $val) $forModel->$col = $forModel->decodeValue($val, $col);
		return $forModel;
	}

	/**
	 * One-to-many relationship
	 *
	 * @param string	$forClass			foreign model class name
	 * @param string	$forColumn			foreign column if it differs from className_id (ex. user_id)
	 * @param string	$condition			condition to append (AND) to the query (use R and F as Reference and Foreign tables)
	 * @param array		$conditionParams	parameters to bind to the condition query
	 *
	 * @return Collection|null collection of foreign models
	 */
	public function hasMany($forClass, $forColumn = null, $condition = "", array $conditionParams = [])
	{
		$refTable = static::tableName();
		$forTable = $forClass::tableName();
		$refColumn = static::$idColumn;
		$forColumn = $forColumn ?: strtolower(static::modelName())."_id";

		$rows = Db::query("
			select F.*
			from $refTable as R, $forTable as F
			where R.$refColumn = F.$forColumn and R.$refColumn = :id $condition
		", array_merge(["id" => $this->$refColumn], $conditionParams), static::getDbName());

		if ($rows === false)
			return false;
		if (empty($rows))
			return new Collection([]);

		// Populate models
		$collection = [];
		foreach ($rows as $row)
		{
			$forModel = new $forClass;
			foreach ($row as $column => $val) $forModel->$column = $forModel->decodeValue($val, $column);
			$collection[] = $forModel;
		}

		return new Collection($collection);
	}

	/**
	 * One-to-one inverse relationship
	 *
	 * @param string $refClass reference model class
	 * @param string $forColumn foreign key if different from refTable_id
	 *
	 * @return Model referenced model or false if fails
	 */
	public function belongsTo($refClass, $forColumn = null)
	{
		// Get columns
		$refTable = $refClass::tableName();
		$forTable = static::tableName();
		$refColumn = $refClass::$idColumn;
		$forColumn = $forColumn ?: strtolower($refClass::modelName())."_id";

		$rows = Db::query("
			select R.*
			from $refTable as R, $forTable as F
			where R.$refColumn = F.$forColumn and F.$forColumn = :id
		", ["id" => $this->$forColumn], static::getDbName());

		if ($rows === false)
			return false;
		if (empty($rows))
			return null;

		$refModel = new $refClass;
		foreach ($rows[0] as $col => $val) $refModel->$col = $refModel->decodeValue($val, $col);
		return $refModel;
	}

	/**
	 * Save object to database, updating or inserting a new row
	 *
	 * @param bool $create for models without an id column this determines if we insert or udpate the model
	 *
	 * @return static|bool false if fails
	 */
	public function save($create = false)
	{
		// Get model informations
		$tableName	= static::tableName();
		$columns	= static::tableColumns();
		$idColumn	= static::$idColumn;

		$isModel	= false;
		$into		= "";
		$values		= "";
		$updates	= "";
		$condition	= "";
		$params		= [];
		$primaries	= [];
		$updateCondition = "";

		// Remove columns for which no value is specified
		foreach ($columns as $i => $column)
		{
			$name	= $column["column_name"];
			$key	= $column["column_key"];

			// Build query components
			if (property_exists($this, $name) && !in_array($name, static::$autos))
			{
				$into		.= "$name, ";
				$values		.= ":$name, ";
				$updates	.= "$name = :$name, ";
				$condition	.= "$name = :$name and ";
				$params[$name] = $this->encodeValue($name);
			}

			// Primary keys used for selecting the correct row in update
			if (strcasecmp($key, "PRI") === 0)
			{
				$updateCondition .= "$name = :$name and ";

				if (property_exists($this, $name) && !in_array($name, static::$autos))
					$primaries[$name] = $this->encodeValue($name);

				// Check if is model with id column
				if ($name === $idColumn) $isModel = true;
			}
		}

		// Remove trailing characters
		$into				= substr($into, 0, -2);
		$values				= substr($values, 0, -2);
		$updates			= substr($updates, 0, -2);
		$condition			= substr($condition, 0, -5);
		$updateCondition	= substr($updateCondition, 0, -5);

		try
		{
			// Try to insert model
			$status = Db::query("
				insert into $tableName ($into)
				values ($values)
			", $params, static::getDbName(), false) !== false;
		}
		catch (PDOException $e)
		{
			// If force creation, then bubble up exception
			if ($create) throw $e;

			// Use exception to determine if it was a primary key conflict
			if ($e->getCode() === "23000" && preg_match("/.*'PRIMARY'$/", $e->getMessage()))
			{
				// Try to update model
				$status = Db::query("
					update $tableName
					set $updates
					where $updateCondition
				", $params, static::getDbName(), false) !== false;
			}
			else
				throw $e;
		}

		if ($status)
		{
			// Get last insert id
			$lastInsertId = Db::instance(static::getDbName())->lastInsertId();

			if ($lastInsertId > 0)
				// Fetch with inserted id
				return static::find($lastInsertId);
			else
				// If no last inserted if try to use update condition and primary keys
				return static::select("where $updateCondition", $primaries, static::getDbName())->first();
		}
		else
			return false;
	}

	/**
	 * Delete this model from table
	 *
	 * @return bool
	 */
	public function delete()
	{
		// Table informations
		$tableName	= static::tableName();
		$columns	= static::tableColumns();
		$idColumn	= static::$idColumn;

		// Use id column if possible
		if (isset($this->$idColumn))
		{
			$status = Db::query("
				delete from $tableName
				where $idColumn = :id
			", ["id" => $this->$idColumn], static::getDbName(), false);
		}
		else
		{
			$condition	= "";
			$params		= [];
			foreach ($columns as $column)
			{
				$name	= $column["column_name"];
				$key	= $column["column_key"];
				if (isset($this->$name))
				{
					$condition .= "$name = :$name and ";
					$params[$name] = $this->encodeValue($name);
				}
			}
			$condition = substr($condition, 0, -5);

			var_dump("
				delete from $tableName
				where $condition
			");
			var_dump($params);

			$status = Db::query("
				delete from $tableName
				where $condition
			", $params, static::getDbName(), false);
		}

		return $status !== false && $status > 0;
	}

	/**
	 * Delete all models that match condition
	 *
	 * @param string	$condition			delete condition
	 * @param array		$conditionParams	condition parameters
	 *
	 * @return bool|int number of rows deleted or false if fails
	 */
	public static function deleteWhere($condition = "", array $conditionParams = [])
	{
		$tableName = static::tableName();

		if (empty($condition))
			return Db::query("delete from $tableName", [], static::getDbName(), false);
		else
			return Db::query("
				delete from $tableName
				where $condition
			", $conditionParams, static::getDbName(), false);
	}

	/**
	 * Purge this model collection
	 *
	 * @return bool query status
	 */
	public static function purge()
	{
		$tableName = static::tableName();
		return Db::query("truncate $tableName", [], static::getDbName(), false) !== false;
	}

	/**
	 * Reset auto increment value
	 *
	 * @return bool query status
	 */
	public static function resetAutoIncrement()
	{
		$tableName = static::tableName();
		return Db::query("alter table $tableName auto_increment = 1", [], static::getDbName(), false) !== false;
	}

	/**
	 * Get and/or set the name of the database connection
	 *
	 * @param string|null $dbName database connection to use
	 *
	 * @return string current connection
	 */
	public static function getDbName($dbName = null)
	{
		// Return name of current connection
		// If no connection is specified by the class
		// use global defined connection
		// If no global connection is defined
		// use "master"
		return !empty(static::$dbName) ? static::$dbName : ($GLOBALS["defaultDbName"] ?? "master");
	}

	/**
	 * Set database connection to use
	 *
	 * @param string|null $dbName database connection to use
	 */
	public static function setDbName($dbName = "master")
	{
		static::$dbName = $dbName;
	}

	/**
	 * Return a json representation of the model
	 *
	 * @return string
	 */
	public function json()
	{
		return to_json($this);
	}

	/**
	 * Return the table name
	 *
	 * @return string
	 */
	public static function tableName()
	{
		// Convert from camel case to underscore
		$cc		= static::modelName();
		$cc[0]	= strtolower($cc[0]);
		return preg_replace_callback("/[A-Z]/", function($uppercase) {

			return "_".strtolower($uppercase[0]);
		}, $cc)."s";
	}

	/**
	 * Return the name of this model
	 *
	 * @return string
	 */
	protected static function modelName()
	{
		return (new ReflectionClass(get_called_class()))->getShortName();
	}

	/**
	 * Convert value coming from database
	 *
	 * @param string	$val	value to convert
	 * @param string	$column	name of the column
	 *
	 * @return mixed
	 */
	protected function decodeValue($val, $column = "")
	{
		if ($column === static::$idColumn)
			$val = (int)$val;
		else if (isset(static::$casts[$column]))
		{
			switch (static::$casts[$column])
			{
				case "object":
					$val = from_json($val, false);
					break;
				case "array":
					$val = from_json($val, true);
					break;
				default:
					settype($val, static::$casts[$column]);
			}
		}
		/// We leave it for compatibility
		else if (in_array($column, static::$jsons) && is_string($val))
			$val = from_json($val);

		return $val;
	}

	/**
	 * Get column value
	 *
	 * @param string $column name of the column
	 *
	 * @return string
	 */
	protected function encodeValue($column)
	{
		/// @todo for compatibility
		$val = in_array($column, static::$jsons) ? to_json($this->$column) : $this->$column;

		// Convert jsons into valid json strings
		if (isset(static::$casts[$column]) && (static::$casts[$column] === "object" || static::$casts[$column] === "array"))
			$val = to_json($this->$column);

		// Convert bools to ints
		if (is_bool($val)) $val = (int)$val;

		return $val;
	}

	/**
	 * Return name of table columns
	 *
	 * @return array|false if query fails
	 */
	protected static function tableColumns()
	{
		// Database config
		global $dbConfig;

		$query = Db::instance(static::getDbName())->prepare("
			select column_name, column_key
			from information_schema.columns
			where table_schema = :schema and table_name = :table
		");
		$query->bindValue(":schema", $dbConfig[static::getDbName()]["schema"]);
		$query->bindValue(":table", static::tableName());

		if ($query->execute())
			return $query->fetchAll(PDO::FETCH_ASSOC);
		else
			return false;
	}
}


<?php

namespace PeskyCMF\Scaffold\MenuItem;

use Swayok\Html\Tag;

class CmfRedirectMenuItem extends CmfMenuItem {

    protected $openOnNewTab = false;

    /**
     * @return $this
     */
    public function openOnNewTab() {
        $this->openOnNewTab = true;
        return $this;
    }

    /**
     * Render menu item as
     * @param bool $withIcon
     * @return string
     */
    public function renderAsButton(bool $withIcon = true): string {
        if ($this->isAccessible()) {
            return $this->wrapIntoShowCondition(
                Tag::a(($withIcon ? $this->makeIcon() . ' ' : '') . $this->getTitle())
                    ->setClass($this->getButtonClasses() . ' ' . $this->makeConditionalDisabledAttribute())
                    ->setHref($this->getUrl())
                    ->setTitle($this->getTooltip())
                    ->setTarget($this->openOnNewTab ? '_blank' : null)
                    ->setDataAttr('toggle', $this->hasTooltip() ? 'tooltip' : null)
                    ->setDataAttr('position', $this->hasTooltip() ? $this->getTooltipPosition() : null)
                    ->build()
            );
        } else {
            return '';
        }
    }

    /**
     * Render menu item as  icon (title will be used as tooltip)
     * @param string $additionalClasses - classes to add to  tag
     * @param bool $allowIconColorClass
     * @return string
     */
    public function renderAsIcon(string $additionalClasses = '', bool $allowIconColorClass = true): string {
        if ($this->isAccessible()) {
            return $this->wrapIntoShowCondition(
                Tag::a($this->makeIcon($allowIconColorClass))
                    ->setClass($additionalClasses)
                    ->setHref($this->getUrl())
                    ->setTitle($this->getTooltipOrTitle())
                    ->setTarget($this->openOnNewTab ? '_blank' : null)
                    ->setDataAttr('toggle', $this->hasTooltipOrTitle() ? 'tooltip' : null)
                    ->setDataAttr('position', $this->hasTooltipOrTitle() ? $this->getTooltipPosition() : null)
                    ->build()
            );
        } else {
            return '';
        }
    }

    /**
     * Render menu item as
...
 or
<button>...</button>

     * @return string
     */
    public function renderAsBootstrapDropdownMenuItem(): string {
        if ($this->isAccessible()) {
            $link = Tag::a($this->makeIcon(true) . ' ' . $this->getTitle())
                ->setHref($this->getUrl())
                ->setTitle($this->getTooltip())
                ->setTarget($this->openOnNewTab ? '_blank' : null)
                ->setDataAttr('toggle', $this->hasTooltip() ? 'tooltip' : null)
                ->setDataAttr('position', $this->hasTooltip() ? $this->getTooltipPosition() : null)
                ->build();
            return $this->wrapIntoShowCondition(
                '
makeConditionalDisabledAttribute() . '>' . $link . '
'
            );
        } else {
            return '';
        }
    }
}


<?php

namespace SnooPHP\Http;

use SnooPHP\Curl\Curl;

/**
 * Http request object
 *
 * @author sneppy
 */
class Request
{
	/**
	 * @var string $url request url
	 */
	protected $url;

	/**
	 * @var string $method request method
	 */
	protected $method;

	/**
	 * @var string $time request timestamp
	 */
	protected $time;

	/**
	 * @var array $headers request headers
	 */
	protected $headers = [];

	/**
	 * @var array $input request input
	 */
	protected $inputs = [];

	/**
	 * @var array $files request input files
	 */
	protected $files = [];

	/**
	 * Create a new request
	 *
	 * @param string	$url		requested url
	 * @param string	$method		request HTTP method
	 * @param string	$time		request timestamp as string
	 * @param array		$headers	request headers
	 * @param array		$inputs		input of the request (GET, POST data)
	 * @param array		$files		files sent with the request
	 */
	public function __construct($url, $method = "GET", $time = null, array $headers = [], array $inputs = [], array $files = [])
	{
		$this->url		= $url;
		$this->method	= $method;
		$this->time		= $time ?: date();
		$this->headers	= $headers;
		$this->inputs	= $inputs;
		$this->files	= $files;
	}

	/**
	 * Return request url
	 *
	 * @return string
	 */
	public function url()
	{
		return $this->url;
	}

	/**
	 * Return request method
	 *
	 * @return string
	 */
	public function method()
	{
		return $this->method;
	}

	/**
	 * Return request timestamp
	 *
	 * @param bool $timestamp if true return as timestamp
	 *
	 * @return string|int
	 */
	public function time($timestamp = false)
	{
		return $timestamp ? (new DateTime($this->time))->getTimestamp() : $this->time;
	}

	/**
	 * Return all request headers or a specific one
	 *
	 * @param string $name header name
	 *
	 * @return string|array|null
	 */
	public function header($name = null)
	{
		return !$name ? $this->headers : ($this->headers[$name] ?? null);
	}

	/**
	 * Return request input
	 *
	 * @param string	$name		input name or null to return whole inputs array
	 * @param mixed		$default	default value returned if such input is found
	 *
	 * @return array|mixed|null
	 */
	public function input($name = null, $default = null)
	{
		return !$name ? $this->inputs : ($this->inputs[$name] ?? $default);
	}

	/**
	 * Return request input files
	 *
	 * @param string $name file name or null to return whole inputs array
	 *
	 * @return array|mixed
	 */
	public function file($name = null)
	{
		return !$name ? $this->files : ($this->files[$name] ?? null);
	}

	/**
	 * Return true if input is valid
	 *
	 * @param array $rules list of input rules
	 *
	 * @return bool
	 */
	public function validateInput(array $rules)
	{
		foreach ($rules as $rule)
			if (empty($this->inputs[$rule])) return false;

		return true;
	}

	/**
	 * Inject input (use carefully)
	 *
	 * @ignore not to be used
	 *
	 * @param array $inputs set of inputs to inject
	 *
	 * @return Request return this request
	 */
	protected function injectInput(array $inputs)
	{
		$this->inputs = array_merge($this->inputs, $inputs);
		return $this;
	}

	/**
	 * Forward this request to another host
	 *
	 * @param string	$host		target host
	 * @param array		$headers	additional headers [default: []]
	 *
	 * @return Curl
	 */
	public function forward($host, array $headers = [])
	{
		// Create and run request
		return Curl::create($this->method, $host.$this->url, $this->inputs, array_merge($this->headers, $headers));
	}

	/**
	 * Return current request
	 *
	 * @return Request
	 */
	public static function current()
	{
		$url		= $_SERVER["REQUEST_URI"];
		$method		= $_SERVER["REQUEST_METHOD"];
		$time		= $_SERVER["REQUEST_TIME_FLOAT"];
		$headers	= getallheaders() ?: [];
		$inputs		= [];
		$files		= [];

		// Populate input
		$raw = [];
		switch ($method)
		{
			case "GET":
				$raw = $_GET;
				break;
			case "POST":
				$raw = $_POST;
				break;
			default:
				parse_str(file_get_contents("php://input"), $raw);
		}
		foreach ($raw as $input => $val) $inputs[$input] = parse_string($val);

		// Populate files
		foreach ($_FILES as $name => $file) $files[$name] = $file;

		// Return request
		return new static(
			$url,
			$method,
			$time,
			$headers,
			$inputs,
			$files
		);
	}

	/**
	 * A quick test request
	 *
	 * @return Request
	 */
	public static function test()
	{
		return new static("/test", "GET", date());
	}
}


<?php

namespace Ekyna\Component\Commerce\Cart\Provider;

use Ekyna\Component\Commerce\Cart\Model\CartInterface;
use Ekyna\Component\Commerce\Cart\Repository\CartRepositoryInterface;
use Ekyna\Component\Commerce\Common\Currency\CurrencyProviderInterface;
use Ekyna\Component\Commerce\Customer\Provider\CustomerProviderInterface;
use Ekyna\Component\Commerce\Exception\RuntimeException;
use Ekyna\Component\Resource\Operator\ResourceOperatorInterface;

/**
 * Class AbstractCartProvider
 * @package Ekyna\Component\Commerce\Cart\Provider
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractCartProvider implements CartProviderInterface
{
    /**
     * @var CartRepositoryInterface
     */
    protected $cartRepository;

    /**
     * @var ResourceOperatorInterface
     */
    protected $cartOperator;

    /**
     * @var CustomerProviderInterface
     */
    protected $customerProvider;

    /**
     * @var CurrencyProviderInterface
     */
    protected $currencyProvider;

    /**
     * @var CartInterface
     */
    protected $cart;

    /**
     * Constructor.
     *
     * @param CartRepositoryInterface $cartRepository
     * @param ResourceOperatorInterface $cartOperator
     * @param CustomerProviderInterface $customerProvider
     * @param CurrencyProviderInterface $currencyProvider
     */
    public function __construct(
        CartRepositoryInterface $cartRepository,
        ResourceOperatorInterface $cartOperator,
        CustomerProviderInterface $customerProvider,
        CurrencyProviderInterface $currencyProvider
    ) {
        $this->cartRepository = $cartRepository;
        $this->cartOperator = $cartOperator;
        $this->customerProvider = $customerProvider;
        $this->currencyProvider = $currencyProvider;
    }

    /**
     * @inheritdoc
     */
    public function hasCart()
    {
        return null !== $this->cart;
    }

    /**
     * @inheritdoc
     */
    public function getCart($create = false)
    {
        if ($create) {
            return $this->createCart();
        }

        return $this->cart;
    }

    /**
     * @inheritdoc
     */
    public function saveCart()
    {
        if (!$this->hasCart()) {
            throw new RuntimeException('Cart has not been initialized yet.');
        }

        $this->cartOperator->persist($this->cart);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function clearCart()
    {
        if (!$this->hasCart() || $this->cart->isLocked()) {
            return $this;
        }

        if (null !== $this->cart->getId()) {
            $this->cartOperator->delete($this->cart, true);
        }

        $this->cart = null;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function clearInformation()
    {
        if (!$this->hasCart() || $this->cart->isLocked()) {
            return $this;
        }

        $this->cart
            ->setCustomer(null)
            ->setCustomerGroup(null)
            ->setEmail(null)
            ->setCompany(null)
            ->setGender(null)
            ->setFirstName(null)
            ->setLastName(null)
            ->setInvoiceAddress(null)
            ->setDeliveryAddress(null)
            ->setSameAddress(true);

        $this->updateCustomerGroupAndCurrency();

        return $this;
    }

    /**
     * Updates the cart customer group and currency.
     *
     * @return $this
     */
    public function updateCustomerGroupAndCurrency()
    {
        if (!$this->hasCart() || $this->cart->isLocked()) {
            return $this;
        }

        // Customer group
        if (null !== $customer = $this->cart->getCustomer()) {
            if ($this->cart->getCustomerGroup() !== $customer->getCustomerGroup()) {
                $this->cart->setCustomerGroup($customer->getCustomerGroup());
            }
        }

        // Sets the default customer group
        if (null === $this->cart->getCustomerGroup()) {
            $this->cart->setCustomerGroup($this->customerProvider->getCustomerGroup());
        }

        // Sets the currency
        if (null === $this->cart->getCurrency()) {
            $this->cart->setCurrency($this->currencyProvider->getCurrency());
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function createCart()
    {
        if ($this->hasCart()) {
            return $this->cart;
        }

        $this->clearCart();

        $this->setCart($this->cartRepository->createNew());

        // Sets the customer if available
        if ($this->customerProvider->hasCustomer()) {
            $this->cart->setCustomer($this->customerProvider->getCustomer());
        }

        $this->updateCustomerGroupAndCurrency();

        $this->cartOperator->initialize($this->cart);

        return $this->cart;
    }

    /**
     * Sets the cart.
     *
     * @param CartInterface $cart
     *
     * @return AbstractCartProvider
     */
    protected function setCart(CartInterface $cart)
    {
        $this->cart = $cart;

        return $this;
    }
}


<?php

namespace mii\web;

use mii\core\Component;

class Response extends Component
{

    // HTTP status codes and messages
    public static $messages = [
        // Informational 1xx
        100 => 'Continue',
        101 => 'Switching Protocols',
        102 => 'Processing',

        // Success 2xx
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',

        // Redirection 3xx
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found', // 1.1
        303 => 'See Other',
        304 => 'Not Modified',
        307 => 'Temporary Redirect',

        // Client Error 4xx
        400 => 'Bad Request',
        401 => 'Unauthorized',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        418 => 'Im a teapot',
        422 => 'Unprocessable Entity',
        429 => 'Too many requests',
        // Server Error 5xx
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported'
    ];

    const FORMAT_RAW = 'raw';
    const FORMAT_HTML = 'html';
    const FORMAT_JSON = 'json';
    const FORMAT_JSONP = 'jsonp';
    const FORMAT_XML = 'xml';

    public $format = self::FORMAT_HTML;

    public $version;

    /**
     * @var  integer     The response http status
     */
    public $status = 200;

    public $status_message = '';

    /**
     * @var  string      The response body
     */
    protected $_content = '';

    protected $_content_type = 'text/html';

    protected $_headers = [];

    /**
     * Adds a new header.
     * If there is already a header with the same name, it will be replaced.
     * @param string $name the name of the header
     * @param string $value the value of the header
     * @return static object itself
     */
    public function set_header($name, $value = '') {
        $name = strtolower($name);
        $this->_headers[$name] = (array)$value;
        return $this;
    }

    /**
     * Adds a new header.
     * If there is already a header with the same name, the new one will
     * be appended to it instead of replacing it.
     * @param string $name the name of the header
     * @param string $value the value of the header
     * @return static the collection object itself
     */
    public function add_header($name, $value) {
        $name = strtolower($name);
        $this->_headers[$name][] = $value;
        return $this;
    }

    public function send_headers() {
        assert(headers_sent() === false, "Headers were already sent");

        if (!isset($this->_headers['content-type'])) {
            $this->set_header('content-type', $this->_content_type . '; charset=UTF-8');
        }

        if ($this->version === null) {
            if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
                $this->version = '1.0';
            } else {
                $this->version = '1.0';
            }
        }

        // Create the response header
        header("HTTP/{$this->version} {$this->status} {$this->status_message}");

        if ($this->_headers) {
            foreach ($this->_headers as $name => $values) {
                $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name)));
                // set replace for first occurrence of header but false afterwards to allow multiple
                $replace = true;
                foreach ($values as $value) {
                    header("$name: $value", $replace);
                    $replace = false;
                }
            }
        }

        return $this;
    }

    /**
     * Sets or gets the HTTP status from this response.
     *
     * @param   integer $status Status to set to this response
     * @return  mixed
     */
    public function status($status = NULL) {
        if ($status === NULL) {
            return $this->status;
        } elseif (array_key_exists($status, Response::$messages)) {
            $this->status = (int)$status;
            $this->status_message = Response::$messages[$this->status];
            return $this;
        } else {
            throw new Exception(__METHOD__ . ' unknown status value : :value', array(':value' => $status));
        }
    }

    public function redirect($url, $code = 302) {

        // todo: process url

        if (\Mii::$app->request->is_ajax()) {
            $this->set_header('X-Redirect', $url);
        } else {
            $this->set_header('Location', $url);
        }

        $this->status($code);

        return $this;
    }

    public function content($content = null) {
        if ($content === null)
            return $this->_content;

        $this->_content = $content;

        return null;
    }

    public function send() {
        switch ($this->format) {
            case self::FORMAT_HTML:
                $this->set_header('content-type', 'text/html; charset=UTF-8');
                break;
            case self::FORMAT_JSON:
                $this->set_header('content-type', 'application/json; charset=UTF-8');
                $this->_content = json_encode($this->_content, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                break;
            case self::FORMAT_XML:
                $this->set_header('content-type', 'application/xml; charset=UTF-8');
                break;
        }

        $this->send_headers();
        $this->send_content();
    }

    protected function send_content() {
        echo $this->_content;
    }
}


<?php

namespace Ekyna\Component\Commerce\Invoice\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;

/**
 * Trait InvoiceSubjectTrait
 * @package Ekyna\Component\Commerce\Invoice\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
trait InvoiceSubjectTrait
{
    /**
     * @var float
     */
    protected $invoiceTotal;

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

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

    /**
     * @var \Doctrine\Common\Collections\Collection|InvoiceInterface[]
     */
    protected $invoices;

    /**
     * Initializes the invoices.
     */
    protected function initializeInvoiceSubject()
    {
        $this->invoiceTotal = 0;
        $this->creditTotal = 0;
        $this->invoiceState = InvoiceStates::STATE_NEW;
        $this->invoices = new ArrayCollection();
    }

    /**
     * Returns the invoice total.
     *
     * @return float
     */
    public function getInvoiceTotal()
    {
        return $this->invoiceTotal;
    }

    /**
     * Sets the invoices total.
     *
     * @param float $total
     *
     * @return $this|InvoiceSubjectInterface
     */
    public function setInvoiceTotal($total)
    {
        $this->invoiceTotal = (float)$total;

        return $this;
    }

    /**
     * Returns the credits total.
     *
     * @return float
     */
    public function getCreditTotal()
    {
        return $this->creditTotal;
    }

    /**
     * Sets the credit total.
     *
     * @param float $total
     *
     * @return $this|InvoiceSubjectInterface
     */
    public function setCreditTotal($total)
    {
        $this->creditTotal = (float)$total;

        return $this;
    }

    /**
     * Returns the invoice state.
     *
     * @return string
     */
    public function getInvoiceState()
    {
        return $this->invoiceState;
    }

    /**
     * Sets the invoice state.
     *
     * @param string $invoiceState
     *
     * @return $this|InvoiceSubjectInterface
     */
    public function setInvoiceState($invoiceState)
    {
        $this->invoiceState = $invoiceState;

        return $this;
    }

    /**
     * Returns whether the order has invoices or not.
     *
     * @return bool
     */
    public function hasInvoices()
    {
        return 0 < $this->invoices->count();
    }

    /**
     * Returns the invoices.
     *
     * @param bool $filter TRUE for invoices, FALSE for credits, NULL for all
     *
     * @return \Doctrine\Common\Collections\Collection|InvoiceInterface[]
     */
    public function getInvoices($filter = null)
    {
        if (null === $filter) {
            return $this->invoices;
        }

        return $this->invoices->filter(function(InvoiceInterface $invoice) use ($filter) {
            return $filter xor InvoiceTypes::isCredit($invoice);
        });
    }

    /**
     * Returns the invoice date.
     *
     * @param bool $latest Whether to return the last invoice date instead of the first.
     *
     * @return \DateTime|null
     */
    public function getInvoicedAt($latest = false)
    {
        if (0 == $this->invoices->count()) {
            return null;
        }

        $criteria = Criteria::create();
        $criteria
            ->andWhere(Criteria::expr()->eq('type', InvoiceTypes::TYPE_INVOICE))
            ->orderBy(['createdAt' => $latest ? Criteria::DESC : Criteria::ASC]);

        /** @var ArrayCollection $invoices */
        $invoices = $this->invoices;
        $invoices = $invoices->matching($criteria);

        /** @var InvoiceInterface $invoice */
        if (false !== $invoice = $invoices->first()) {
            return $invoice->getCreatedAt();
        }

        return null;
    }
}


<?php

namespace PeskyCMF\Scaffold\ItemDetails;

use PeskyORM\Core\DbExpr;
use PeskyORM\ORM\Relation;
use Swayok\Html\Tag;

class ManyToManyRelationRecordsValueCell extends ValueCell {

    protected $columnForLinksLabels;
     /** @var Relation|null */
    private $dataSourceRelation;

    /**
     * Name of the column in 'linker table' that contains primary key values of the foreign table
     * For example you have 3 tables: items (main table), categories (foreing table), item_categories (linker table);
     * Here you have many-to-many relation between items and categories that resolved via 'linker table'
     * item_categories that contains only 3 columns: id, item_id (link to items.id), category_id (link to categoris.id);
     * You need to pass 'category_id' via $columnName argument
     * @param $columnName
     * @return $this
     */
    public function setRelationsLinkingColumn($columnName) {
        $this->relationColumn = $columnName;
        return $this;
    }

    public function setRelation(Relation $relation, $columnName) {
        $this->relation = $relation;
        if ($columnName !== $relation->getForeignColumnName()) {
            $this->relationColumn = $columnName;
        }
        $this->dataSourceRelation = null;
        return $this;
    }

    public function getRelationColumn() {
        if (empty($this->relationColumn)) {
            throw new \UnexpectedValueException(
                "Relations linking column was not provided for '{$this->getName()}' input. "
                . 'Use setRelationsLinkingColumn(\'column_name\') method to provide it'
            );
        }
        return parent::getRelationColumn();
    }

    public function getAdditionalRelationsToRead() {
        return [$this->getRelation()->getName() => ['*', $this->getDataSourceRelation()->getName() => ['*']]];
    }

    public function doDefaultValueConversionByType($value, $type, array $record) {
        if (is_array($value) && !empty($value)) {
            $links = [];
            $dataSourceRelation = $this->getDataSourceRelation();
            $labelColumn = $this->getColumnForLinksLabels($dataSourceRelation->getDisplayColumnName());
            $tableNameForRoute = $this->getTableNameForRouteToRelatedRecord() ?: $dataSourceRelation->getForeignTable()->getName();
            foreach ($value as $relatedRecord) {
                $links[] = Tag::a()
                    ->setHref(routeToCmfItemDetails(
                        $tableNameForRoute,
                        $relatedRecord[$this->getRelationColumn()]
                    ))
                    ->setContent($labelColumn instanceof \Closure
                        ? $labelColumn($relatedRecord[$dataSourceRelation->getName()])
                        : $relatedRecord[$dataSourceRelation->getName()][$labelColumn]
                    )
                    ->build();
            }
            return implode('<br>', $links);
        }
        return '';
    }

    /**
     * @return Relation
     * @throws \InvalidArgumentException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     * @throws \BadMethodCallException
     * @throws \UnexpectedValueException
     */
    protected function getDataSourceRelation() {
        if (!$this->dataSourceRelation) {
            $relations = $this
                ->getRelation()
                ->getForeignTable()
                ->getTableStructure()
                ->getColumn($this->getRelationColumn())
                ->getRelations();
            foreach ($relations as $relation) {
                if ($relation->getType() === Relation::HAS_ONE) {
                    $this->dataSourceRelation = $relation;
                    break;
                }
            }
            if (!$this->dataSourceRelation) {
                throw new \UnexpectedValueException(
                    "Failed to detect data source Relation for ManyToManyRelationRecordsFormInput '{$this->getName()}'. "
                    . "Column '{$this->getRelation()->getForeignTable()->getName()}.{$this->getRelationColumn()}'"
                    . ' has no Relation with type HAS ONE'
                );
            }
        }
        return $this->dataSourceRelation;
    }

    /**
     * Set column name or closure that will be used as label for links
     * @param string|\Closure $columnNameOrClosure
     *      - string: column name
     *      - \Closure: function (RecordInterface $record) { return 'value' }
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setColumnForLinksLabels($columnNameOrClosure) {
        if (!is_string($columnNameOrClosure) && !($columnNameOrClosure instanceof DbExpr)) {
            throw new \InvalidArgumentException(
                '$columnNameOrClosure argument must be a string or a closure'
            );
        }
        $this->columnForLinksLabels = $columnNameOrClosure;
        return $this;
    }

    /**
     * @param mixed $default
     * @return string|DbExpr|null
     */
    protected function getColumnForLinksLabels($default = null) {
        return $this->columnForLinksLabels ?: $default;
    }

}


<?php

namespace PeskyCMF\Console\Commands;

use Illuminate\Console\Command;
use PeskyCMF\Config\CmfConfig;
use PeskyCMF\PeskyCmfManager;
use PeskyCMF\Scaffold\KeyValueTableScaffoldConfig;
use PeskyCMF\Scaffold\NormalTableScaffoldConfig;
use PeskyORM\ORM\ClassBuilder;
use PeskyORM\ORM\Column;
use PeskyORM\ORM\Table;
use PeskyORM\ORM\TableInterface;
use Swayok\Utils\File;
use Swayok\Utils\StringUtils;

class CmfMakeScaffoldCommand extends Command {

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'cmf:make-scaffold
        {table_name}
        {cmf-section? : cmf section name (key) that exists in config(\'peskycmf.cmf_configs\') and accessiblr by PeskyCmfManager}
        {--resource= : name of resource if it differs from table_name}
        {--cmf-config-class= : full class name to a class that extends CmfConfig}
        {--class-name= : short scaffold class name}
        {--keyvalue : table is key-value storage}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create scaffold class for DB table.';

    /**
     * @var CmfConfig
     */
    protected $cmfConfigClass;

    protected function getScaffoldConfigParentClass() {
        if ($this->option('keyvalue')) {
            return $this->getCmfConfigClass()->config('ui.scaffold_configs_base_class_for_key_value_tables') ?: KeyValueTableScaffoldConfig::class;
        } else {
            return $this->getCmfConfigClass()->config('ui.scaffold_configs_base_class') ?: NormalTableScaffoldConfig::class;
        }
    }

    /**
     * @return CmfConfig
     * @throws \InvalidArgumentException
     */
    protected function getCmfConfigClass() {
        if (!$this->cmfConfigClass) {
            $class = $this->option('cmf-config-class');
            if ($class) {
                if (!class_exists($class)) {
                    throw new \InvalidArgumentException(
                        'Class ' . $class . ' provided through option --cmf-config-class does not exist'
                    );
                }
                if (!is_subclass_of($class, CmfConfig::class)) {
                    throw new \InvalidArgumentException(
                        'Class ' . $class . ' provided through option --cmf-config-class must extend CmfConfig class'
                    );
                }
                /** @var CmfConfig $class */
                $this->cmfConfigClass = $class::getInstance();
            } else {
                $sectionName = $this->argument('cmf-section');
                if (!empty($sectionName)) {
                    /** @var PeskyCmfManager $peskyCmfManager */
                    $peskyCmfManager = app(PeskyCmfManager::class);
                    $this->cmfConfigClass = $peskyCmfManager->getCmfConfigForSection($sectionName);
                } else {
                    $this->cmfConfigClass = CmfConfig::getDefault();
                    if (get_class($this->cmfConfigClass) === CmfConfig::class) {
                        throw new \InvalidArgumentException(
                            'Child class for CmfConfig was not found. You need to provide it through --cmf-config-class option '
                        );
                    }
                }
            }
        }
        return $this->cmfConfigClass;
    }

    /**
     * @param TableInterface $table
     * @param string|null $resourceName
     * @return string - short name of scaffold class to be created
     */
    protected function getScaffoldClassName(TableInterface $table, ?string $resourceName = null) {
        $scaffoldClassName = $this->option('class-name');
        if (empty($scaffoldClassName)) {
            return (empty($resourceName) ? $table::getAlias() : StringUtils::classify($resourceName)) . 'ScaffoldConfig';
        }
        return $scaffoldClassName;
    }

    protected function getTableObjectByTableName(string $tableName): TableInterface {
        /** @var ClassBuilder $builderClass */
        $builderClass = config('peskyorm.class_builder');
        $dbClassesNamespace = rtrim(config('peskyorm.classes_namespace', 'App\\Db'), '\\') . '\\';
        /** @var Table $className */
        $className = $dbClassesNamespace . StringUtils::classify($tableName) . '\\' . $builderClass::makeTableClassName($tableName);
        return $className::getInstance();
    }

    /**
     * Execute the console command.
     */
    public function handle() {
        $table = $this->getTableObjectByTableName($this->argument('table_name'));

        $namespace = $this->getScaffoldsNamespace();
        $className = $this->getScaffoldClassName($table, $this->option('resource'));

        $filePath = $this->getFolder($namespace) . $className . '.php';
        if (File::exist($filePath)) {
            if ($this->confirm("Scaffold class file {$filePath} already exists. Overwrite?")) {
                File::remove($filePath);
            } else {
                $this->line('Terminated');
                return;
            }
        }

        $this->createScaffoldClassFile($table, $namespace, $className, $filePath);

        $this->line($filePath . ' created');

        $columnsTranslations = [];
        foreach ($table->getTableStructure()->getColumns() as $column) {
            $columnsTranslations[] = "'{$column->getName()}' => ''";
        }
        $columnsTranslationsFilter = implode(",\n                    ", $columnsTranslations) . ",";
        $columnsTranslations = implode(",\n                ", $columnsTranslations) . ",";

        $this->comment(<<<INFO

Translations:
    '{$table->getTableStructure()->getTableName()}' => [
        'menu_title' => '',
        'datagrid' => [
            'header' => '',
            'column' => [
                $columnsTranslations
            ],
            'filter' => [
                '{$table->getTableStructure()->getTableName()}' => [
                    $columnsTranslationsFilter
                ],
            ],
        ],
        'form' => [
            'header_create' => '',
            'header_edit' => '',
            'input' => [
                $columnsTranslations
            ],
        ],
        'item_details' => [
            'header' => '',
            'field' => [
                $columnsTranslations
            ],
        ]
    ]

INFO
);
    }

    /**
     * @return string
     */
    protected function getScaffoldsNamespace() {
        $appSubfolder = str_replace('/', '\\', $this->getCmfConfigClass()->app_subfolder());
        $scaffoldsSubfolder = str_replace('/', '\\', $this->getScaffoldsFolderName());
        return 'App\\' . $appSubfolder . '\\' . $scaffoldsSubfolder;
    }

    /**
     * @param string $table
     * @return string
     * @throws \ReflectionException
     */
//    protected function getNamespaceByTable($table) {
//        $namespace = (new \ReflectionClass($table))->getNamespaceName();
//        return preg_replace('%^PeskyCM[FS]\\\Db\\\%', config('peskyorm.classes_namespace', '\\App\\Db') . '\\', $namespace);
//    }

    /**
     * @return string
     */
    protected function getScaffoldsFolderName() {
        return 'Scaffolds';
    }

    /**
     * @param string $namespace
     * @return string
     */
    protected function getFolder($namespace) {
        $appSubfolder = str_replace('/', '\\', $this->getCmfConfigClass()->app_subfolder());
        return static::getBasePathToApp() . DIRECTORY_SEPARATOR . $appSubfolder . DIRECTORY_SEPARATOR . $this->getScaffoldsFolderName() . DIRECTORY_SEPARATOR;
        /*$folder = preg_replace(
            ['%[\\/]%', '%^/?App%'],
            [DIRECTORY_SEPARATOR, $this->getBasePathToApp()],
            rtrim($namespace, '\\')
        );
        return $folder . DIRECTORY_SEPARATOR;*/
    }

    /**
     * @return string
     */
    protected function getBasePathToApp() {
        return app_path();
    }

    /**
     * @param TableInterface $table
     * @param string $namespace
     * @param string $className
     * @param string $filePath
     */
    protected function createScaffoldClassFile(TableInterface $table, $namespace, $className, $filePath) {
        $parentClass = $this->getScaffoldConfigParentClass();
        $parentClassShort = class_basename($parentClass);
        $tableClass = get_class($table);
        $tableClassShort = class_basename($table);

        $contents = <<<VIEW
<?php

namespace $namespace;

use {$parentClass};
use {$tableClass};
use PeskyCMF\Scaffold\DataGrid\DataGridColumn;
use PeskyCMF\Scaffold\Form\FormInput;
use PeskyCMF\Scaffold\Form\InputRenderer;
use PeskyCMF\Scaffold\ItemDetails\ValueCell;

class {$className} extends {$parentClassShort} {

    protected \$isDetailsViewerAllowed = true;
    protected \$isCreateAllowed = true;
    protected \$isEditAllowed = true;
    protected \$isCloningAllowed = false;
    protected \$isDeleteAllowed = true;

    static public function getTable() {
        return {$tableClassShort}::getInstance();
    }

    static protected function getIconForMenuItem() {
        return ''; //< icon classes like: 'fa fa-cog' or just delete if you do not want an icon
    }

    protected function createDataGridConfig() {
        return parent::createDataGridConfig()
            ->readRelations([
                {$this->makeContainsForDataGrid($table)}
            ])
            ->setOrderBy('{$table::getPkColumnName()}', 'asc')
            ->setColumns([
                {$this->makeFieldsListForDataGrid($table)}
            ]);
    }

    protected function createDataGridFilterConfig() {
        return parent::createDataGridFilterConfig()
            ->setFilters([
                {$this->makeFiltersList($table)}
            ]);
    }

    protected function createItemDetailsConfig() {
        return parent::createItemDetailsConfig()
            ->readRelations([
                {$this->makeContainsForItemDetailsViewer($table)}
            ])
            ->setValueCells([
                {$this->makeFieldsListForItemDetailsViewer($table)}
            ]);
    }

    protected function createFormConfig() {
        return parent::createFormConfig()
            ->setWidth(50)
            ->setFormInputs([
                {$this->makeFieldsListForItemForms($table)}
            ]);
    }
}
VIEW;
        File::save($filePath, $contents, 0664, 0755);
    }

    protected function makeContainsForDataGrid(TableInterface $table) {
        $contains = [];
        foreach ($this->getJoinableRelationNames($table) as $relationName) {
            $contains[] = "'{$relationName}' => ['*'],";
        }
        return implode("\n                ", $contains);
    }

    protected function makeContainsForItemDetailsViewer(TableInterface $table) {
        $contains = [];
        foreach ($this->getJoinableRelationNames($table) as $relationName) {
            $contains[] = "'{$relationName}' => ['*'],";
        }
        return implode('', $contains);
    }

    protected function getJoinableRelationNames(TableInterface $table) {
        $ret = [];
        foreach ($table->getTableStructure()->getRelations() as $relation) {
            if ($relation->getType() !== $relation::HAS_MANY) {
                $ret[] = $relation->getName();
            }
        }
        return $ret;
    }

    protected function makeFieldsListForDataGrid(TableInterface $table) {
        $valueViewers = [];
        foreach ($table->getTableStructure()->getColumns() as $column) {
            if ($column->isItAForeignKey()) {
                $valueViewers[] = <<<VIEW
'{$column->getName()}' => DataGridColumn::create()
                    ->setType(DataGridColumn::TYPE_LINK),
VIEW;
            } else if (!in_array($column->getType(), [Column::TYPE_TEXT, Column::TYPE_JSON, Column::TYPE_JSONB, Column::TYPE_BLOB], true)){
                $valueViewers[] = "'{$column->getName()}',";
            }
        }
        return implode("\n                ", $valueViewers);
    }

    protected function makeFiltersList(TableInterface $table) {
        $valueViewers = [];
        foreach ($table->getTableStructure()->getColumns() as $column) {
            if (!in_array($column->getType(), [Column::TYPE_TEXT, Column::TYPE_JSON, Column::TYPE_JSONB, Column::TYPE_BLOB], true)) {
                $valueViewers[] = "'{$column->getName()}',";
            }
        }
        return implode("\n                ", $valueViewers);
    }

    protected function makeFieldsListForItemDetailsViewer(TableInterface $table) {
        $valueViewers = [];
        foreach ($table->getTableStructure()->getColumns() as $column) {
            if ($column->isItAForeignKey()) {
                $valueViewers[] = <<<VIEW
'{$column->getName()}' => ValueCell::create()
                    ->setType(ValueCell::TYPE_LINK),
VIEW;
            } else {
                $valueViewers[] = "'{$column->getName()}',";
            }
        }
        return implode("\n                ", $valueViewers);
    }

    protected function makeFieldsListForItemForms(TableInterface $table) {
        $valueViewers = [];
        foreach ($table->getTableStructure()->getColumns() as $column) {
            if ($column->isValueCanBeSetOrChanged() && !$column->isAutoUpdatingValue() && !$column->isItPrimaryKey()) {
                if ($column->getName() === 'admin_id') {
                    $valueViewers[] = <<<VIEW
'{$column->getName()}' => FormInput::create()
                    ->setType(FormInput::TYPE_HIDDEN)
                    ->setSubmittedValueModifier(function () {
                        return static::getUser()->id;
                    }),
VIEW;

                } else {
                    $valueViewers[] = "'{$column->getName()}',";
                }
            }
        }
        return implode("\n                ", $valueViewers);
    }

}


<?php

namespace Ekyna\Component\Commerce\Common\Listener;

use Ekyna\Component\Commerce\Common\Model\NotificationTypes;
use Ekyna\Component\Commerce\Order\Model\OrderInterface;
use Ekyna\Component\Commerce\Order\Model\OrderStates;

/**
 * Class OrderNotifyListener
 * @package Ekyna\Component\Commerce\Common\Listener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class OrderNotifyListener extends AbstractNotifyListener
{
    /**
     * Post persist event handler.
     *
     * @param OrderInterface $order
     */
    public function postPersist(OrderInterface $order)
    {
        $this->watch($order);
    }

    /**
     * Post update event handler.
     *
     * @param OrderInterface $order
     */
    public function postUpdate(OrderInterface $order)
    {
        $this->watch($order);
    }

    /**
     * Order watcher.
     *
     * @param OrderInterface $order
     */
    protected function watch(OrderInterface $order)
    {
        // Abort if notify disabled or sample order
        if (!$order->isAutoNotify() || $order->isSample()) {
            return;
        }

        // Abort if sale has notification of type 'SALE_ACCEPTED'
        if ($order->hasNotifications(NotificationTypes::ORDER_ACCEPTED)) {
            return;
        }

        // Abort if state has not changed for 'ACCEPTED'
        if (!$this->didStateChangeTo($order, OrderStates::STATE_ACCEPTED)) {
            return;
        }

        $this->notify(NotificationTypes::ORDER_ACCEPTED, $order);
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

use Ekyna\Component\Commerce\Exception\InvalidArgumentException;

/**
 * Class AdjustmentModes
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class AdjustmentModes
{
    const MODE_FLAT    = 'flat';
    const MODE_PERCENT = 'percent';

    /**
     * Returns all the modes.
     *
     * @return array
     */
    static public function getModes()
    {
        return [
            static::MODE_FLAT,
            static::MODE_PERCENT,
        ];
    }

    /**
     * Returns whether the given mode is valid or not.
     *
     * @param string $mode
     * @param bool   $throw
     *
     * @return bool
     */
    static public function isValidMode($mode, $throw = true)
    {
        if (in_array($mode, static::getModes(), true)) {
            return true;
        }

        if ($throw) {
            throw new InvalidArgumentException('Invalid adjustment mode.');
        }

        return false;
    }
}


<?php

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

use Doctrine\DBAL\Types\Type;
use Ekyna\Component\Commerce\Payment\Model\PaymentMethodInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Repository\PaymentRepositoryInterface;
use Ekyna\Component\Resource\Doctrine\ORM\ResourceRepository;

/**
 * Class AbstractPaymentRepository
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractPaymentRepository extends ResourceRepository implements PaymentRepositoryInterface
{
    /**
     * @var \Doctrine\ORM\Query
     */
    private $byMethodAndStatesFromDateQuery;

    /**
     * @inheritDoc
     */
    public function findOneByKey($key)
    {
        return $this->findOneBy(['key' => $key]);
    }

    /**
     * @inheritDoc
     */
    public function findByMethodAndStates(PaymentMethodInterface $method, array $states, \DateTime $fromDate = null)
    {
        foreach ($states as $state) {
            PaymentStates::isValidState($state);
        }

        if (null === $fromDate) {
            $fromDate = new \DateTime();
            $fromDate->modify('-200 years'); // At least ! xD
        }

        return $this
            ->getByMethodAndStatesFromDateQuery()
            ->setParameter('method', $method)
            ->setParameter('date', $fromDate, Type::DATE)
            ->setParameter('states', $states)
            ->getResult();
    }

    /**
     * Returns the "find by method and states from date" query.
     *
     * @return \Doctrine\ORM\Query
     */
    private function getByMethodAndStatesFromDateQuery()
    {
        if (null !== $this->byMethodAndStatesFromDateQuery) {
            return $this->byMethodAndStatesFromDateQuery;
        }

        $qb = $this->createQueryBuilder('p');

        $query = $qb
            ->andWhere($qb->expr()->eq('p.method', ':method'))
            ->andWhere($qb->expr()->in('p.state', ':states'))
            ->andWhere($qb->expr()->gte('p.createdAt', ':date'))
            ->addOrderBy('p.createdAt', 'ASC')
            ->getQuery()
            ->useQueryCache(true);

        return $this->byMethodAndStatesFromDateQuery = $query;
    }

    /**
     * @inheritDoc
     */
    public function findByMonth(\DateTime $date, array $states)
    {
        foreach ($states as $state) {
            PaymentStates::isValidState($state);
        }

        $qb = $this->createQueryBuilder('p');

        $start = clone $date;
        $start->modify('first day of this month');
        $start->setTime(0,0,0);

        $end = clone $date;
        $end->modify('last day of this month');
        $end->setTime(23,59,59);

        return $qb
            ->andWhere($qb->expr()->between('p.completedAt', ':start', ':end'))
            ->andWhere($qb->expr()->in('p.state', ':states'))
            ->addOrderBy('p.completedAt', 'ASC')
            ->getQuery()
            ->setParameter('start', $start, Type::DATETIME)
            ->setParameter('end', $end, Type::DATETIME)
            ->setParameter('states', $states)
            ->getResult();
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

use Ekyna\Component\Commerce\Cart\Model\CartInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Order\Model\OrderInterface;
use Ekyna\Component\Commerce\Order\Model\OrderStates;
use Ekyna\Component\Commerce\Quote\Model\QuoteInterface;

/**
 * Class TransformationTargets
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class TransformationTargets
{
    const TARGET_CART  = 'cart';
    const TARGET_QUOTE = 'quote';
    const TARGET_ORDER = 'order';

    /**
     * Returns the available targets for the given sale.
     *
     * @param SaleInterface $sale
     *
     * @return array
     */
    static public function getTargetsForSale(SaleInterface $sale)
    {
        if ($sale instanceof CartInterface) {
            return [static::TARGET_ORDER, static::TARGET_QUOTE];
        } elseif ($sale instanceof OrderInterface) {
            if ($sale->getState() !== OrderStates::STATE_NEW) {
                return [];
            }
            return [static::TARGET_QUOTE];
        } elseif ($sale instanceof QuoteInterface) {
            return [static::TARGET_ORDER];
        }

        throw new InvalidArgumentException("Unexpected sale type.");
    }

    /**
     * Returns whether the target is validfor the sale.
     *
     * @param string        $target
     * @param SaleInterface $sale
     *
     * @return bool
     */
    static public function isValidTargetForSale($target, SaleInterface $sale)
    {
        return in_array($target, static::getTargetsForSale($sale), true);
    }

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


<?php

namespace PeskyCMF;

use PeskyCMF\Db\Settings\CmfSettingsTable;
use PeskyCMF\Scaffold\Form\FormConfig;
use PeskyCMF\Scaffold\Form\FormInput;
use PeskyCMF\Scaffold\Form\KeyValueSetFormInput;
use PeskyORM\Core\DbExpr;

/**
 * @method static string default_browser_title($default = null, $ignoreEmptyValue = true)
 * @method static string browser_title_addition($default = null, $ignoreEmptyValue = true)
 * @method static array languages($default = null, $ignoreEmptyValue = true)
 * @method static string default_language($default = null, $ignoreEmptyValue = true)
 * @method static array fallback_languages($default = null, $ignoreEmptyValue = true)
 */
class PeskyCmfAppSettings {

    /** @var $this */
    static protected $instance;

    const DEFAULT_BROWSER_TITLE = 'default_browser_title';
    const BROWSER_TITLE_ADDITION = 'browser_title_addition';
    const LANGUAGES = 'languages';
    const DEFAULT_LANGUAGE = 'default_language';
    const FALLBACK_LANGUAGES = 'fallback_languages';

    /** @var null|array */
    static protected $loadedMergedSettings;
    /** @var null|array */
    static protected $loadedDbSettings;

    static protected $settingsForWysiwygDataIsnserts = [

    ];

    /**
     * @return $this
     */
    static public function getInstance() {
        if (!static::$instance) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    protected function __construct() {
    }

    static public function getSettingsForWysiwygDataIsnserts() {
        return static::$settingsForWysiwygDataIsnserts;
    }

    /**
     * Get form inputs for app settings (used in CmfSettingsScaffoldConfig)
     * You can use setting name as form input - it will be simple text input;
     * In order to make non-text input - use instance of FormInput class or its descendants as value and
     * setting name as key;
     * @param FormConfig $scaffold
     * @return FormConfig
     */
    static public function configureScaffoldFormConfig(FormConfig $scaffold) {
        return $scaffold
            ->addTab($scaffold->translate(null, 'tab.general'), [
                static::DEFAULT_BROWSER_TITLE,
                static::BROWSER_TITLE_ADDITION,
            ])
            ->addTab($scaffold->translate(null, 'tab.localization'), [
                static::LANGUAGES => KeyValueSetFormInput::create()
                    ->setMinValuesCount(1)
                    ->setAddRowButtonLabel($scaffold->translate(null, 'input.languages_add'))
                    ->setDeleteRowButtonLabel($scaffold->translate(null, 'input.languages_delete')),
                static::DEFAULT_LANGUAGE => FormInput::create()
                    ->setType(FormInput::TYPE_SELECT)
                    ->setOptions(function () {
                        return static::languages();
                    }),
                static::FALLBACK_LANGUAGES => KeyValueSetFormInput::create()
                    ->setAddRowButtonLabel($scaffold->translate(null, 'input.fallback_languages_add'))
                    ->setDeleteRowButtonLabel($scaffold->translate(null, 'input.fallback_languages_delete')),
            ]);
    }

    /**
     * @return array
     */
    static public function getValidatorsForScaffoldFormConfig() {
        return [
            static::DEFAULT_LANGUAGE => 'required|string|size:2|alpha|regex:%^[a-zA-Z]{2}$%|in:' . implode(',', array_keys(static::languages())),
            static::LANGUAGES . '.*.key' => 'required|string|size:2|alpha|regex:%^[a-zA-Z]{2}$%',
            static::LANGUAGES . '.*.value' => 'required|string|max:88',
            static::FALLBACK_LANGUAGES . '.*.key' => 'required|string|size:2|alpha|regex:%^[a-zA-Z]{2}$%',
            static::FALLBACK_LANGUAGES . '.*.value' => 'required|string|size:2|alpha|regex:%^[a-zA-Z]{2}$%'
        ];
    }

    /**
     * Get all validators for specific key.
     * Override this if you need some specific validation for keys that are not present in scaffold config form and
     * can only be updated via static::update() method.
     * @param string $key
     * @return array
     */
    static protected function getValidatorsForKey($key) {
        $validators = static::getValidatorsForScaffoldFormConfig();
        $validatorsForKey = [];
        foreach ($validators as $setting => $rules) {
            if (preg_match("%^{$key}($|\.)%", $setting)) {
                $validatorsForKey[] = $rules;
            }
        }
        return $validatorsForKey;
    }

    /**
     * Passed to FormConfig->setIncomingDataModifier()
     * @param array $data
     * @return array
     */
    static public function modifyIncomingData(array $data) {
        return $data;
    }

    private $defaultValues;
    /**
     * Get default value for setting $name
     * @param string $name
     * @return mixed
     */
    static public function getDefaultValue($name) {
        if (!static::getInstance()->defaultValues) {
            static::getInstance()->defaultValues = static::getAllDefaultValues();
        }
        if (array_has(static::getInstance()->defaultValues, $name)) {
            if (static::getInstance()->defaultValues[$name] instanceof \Closure) {
                static::getInstance()->defaultValues[$name] = static::getInstance()->defaultValues[$name]();
            }
            return static::getInstance()->defaultValues[$name];
        }
        return null;
    }

    /**
     * @return array
     */
    static public function getAllDefaultValues() {
        return [
            static::LANGUAGES => ['en' => 'English'],
            static::DEFAULT_LANGUAGE => 'en',
            static::FALLBACK_LANGUAGES => []
        ];
    }

    /**
     * @return CmfSettingsTable
     */
    static protected function getTable() {
        return app(CmfSettingsTable::class);
    }

    /**
     * @param bool $ignoreCache
     * @return array
     */
    static public function getAllValues($ignoreCache = false) {
        $settings = static::getTable()->getValuesForForeignKey(null, $ignoreCache, true);
        foreach (static::getAllDefaultValues() as $name => $defaultValue) {
            if (!array_key_exists($name, $settings) || $settings[$name] === null) {
                $settings[$name] = value($defaultValue);
            }
        }
        return $settings;
    }

    /** @noinspection MagicMethodsValidityInspection */
    public function __get($name) {
        static::$name();
    }

    static public function __callStatic($name, $arguments) {
        $default = array_get($arguments, 0, static::getDefaultValue($name));
        $ignoreEmptyValue = (bool)array_get($arguments, 1, true);
        return static::getTable()->getValue($name, null, $default, $ignoreEmptyValue);
    }

    public function __call($name, $arguments) {
        /** @noinspection ImplicitMagicMethodCallInspection */
        return static::__callStatic($name, (array)$arguments);
    }

    /**
     * @param string $key
     * @param mixed $value
     * @param bool $validate
     * @throws \InvalidArgumentException
     */
    static public function update($key, $value, $validate = true) {
        $data = [$key => $value];
        if ($validate && !($value instanceof DbExpr)) {
            $rules = static::getValidatorsForKey($key);
            if (!empty($rules)) {
                $validator = \Validator::make($data, $rules);
                if ($validator->fails()) {
                    throw new \InvalidArgumentException(
                        "Invalid value received for setting '$key'. Errors: "
                        . var_export($validator->getMessageBag()->toArray(), true)
                    );
                }
            }
        }
        $table = static::getTable();
        $table::updateOrCreateRecord($table::makeDataForRecord($key, $value));
    }

    /**
     * @param string $key
     */
    static public function delete($key) {
        static::getTable()->delete([static::getTable()->getKeysColumnName() => $key]);
    }

}


<?php namespace TheLHC\IpInfo;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class IpInfoRepository implements IpInfoInterface
{
    private $error;

    /**
     * Query Ip lookup service for $ip
     *
     * @param  string $ip
     * @return stdClass
     */
    public function lookup($ip)
    {
        if ($ip == '127.0.0.1') {
          return (object)[
            'status'  => 'error',
            'errorMessage'  => 'Cannot lookup localhost'
          ];
        }
        if ($this->isReserved($ip)) {
            return (object)[
              'status'  => 'error',
              'errorMessage'  => 'Cannot lookup reserved ip'
            ];
        }
        $client = new Client();
        try {
          $response = $client->request(
            'GET',
            "http://ipinfo.io/{$ip}"
          );
          if ($response->getStatusCode() == "200") {
              return (object)[
                  'status'  => 'success',
                  'results' => json_decode($response->getBody()->getContents())
              ];
          } else {
              return (object)[
                'status'  => 'error',
                'errorMessage'  => $response->getStatusCode()
              ];
          }
        } catch (RequestException $e) {
            $this->error = $e->getMessage();
            return (object)[
              'status'  => 'error',
              'errorMessage'  => $this->error
            ];
        }
    }

    public function isReserved($ip)
    {
        $ipNum = "";
        $pieces = preg_split("/\./", $ip);
        foreach($pieces as $index => $piece) {
            $prefZeros = (3 - strlen($piece));
            for ($i = 1; $i <= $prefZeros; $i++) {
                $ipNum .= "0";
            }
            $ipNum .= $piece;
        }
        // cast to integer
        $ipNum = (int)$ipNum;

        if (
            ($ipNum >= 10000000000 && $ipNum <= 10255255255) or
            ($ipNum >= 172016000000 && $ipNum <= 172031255255) or
            ($ipNum >= 192168000000 && $ipNum <= 192168255255)
        ) {
            return true;
        } else {
            return false;
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Payment\Model;

/**
 * Class PaymentTransitions
 * @package Ekyna\Component\Commerce\Payment\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class PaymentTransitions
{
    const TRANSITION_CANCEL = 'cancel';
    const TRANSITION_HANG   = 'hang';
    const TRANSITION_ACCEPT = 'accept';
    const TRANSITION_REJECT = 'reject';
    const TRANSITION_REFUND = 'refund';

    /**
     * Returns the available payment transitions.
     *
     * @param PaymentInterface $payment
     * @param bool             $admin
     *
     * @return array
     */
    static function getAvailableTransitions(PaymentInterface $payment, $admin = false)
    {
        // TODO use payum to select gateway's supported requests/actions.

        $transitions = [];

        $method = $payment->getMethod();
        $state = $payment->getState();

        if ($admin) {
            if ($method->isManual()) {
                switch ($state) {
                    case PaymentStates::STATE_PENDING:
                        $transitions[] = static::TRANSITION_CANCEL;
                        $transitions[] = static::TRANSITION_ACCEPT;
                        break;
                    case PaymentStates::STATE_CAPTURED:
                        $transitions[] = static::TRANSITION_CANCEL;
                        $transitions[] = static::TRANSITION_HANG;
                        $transitions[] = static::TRANSITION_REFUND;
                        break;
                    case PaymentStates::STATE_REFUNDED:
                        $transitions[] = static::TRANSITION_CANCEL;
                        $transitions[] = static::TRANSITION_HANG;
                        $transitions[] = static::TRANSITION_ACCEPT;
                        break;
                    case PaymentStates::STATE_CANCELED:
                        $transitions[] = static::TRANSITION_HANG;
                        $transitions[] = static::TRANSITION_ACCEPT;
                        break;
                }
            } elseif ($method->isOutstanding() || $method->isManual()) {
                if ($state === PaymentStates::STATE_CAPTURED) {
                    $transitions[] = static::TRANSITION_CANCEL;
                } else {
                    $transitions[] = static::TRANSITION_ACCEPT;
                }
            } else {
                if ($state === PaymentStates::STATE_CAPTURED) {
                    $transitions[] = static::TRANSITION_REFUND;
                }
                if ($state === PaymentStates::STATE_PENDING) {
                    $diff = $payment->getUpdatedAt()->diff(new \DateTime());
                    if (0 < $diff->days && !$diff->invert) {
                        $transitions[] = static::TRANSITION_CANCEL;
                    }
                }
            }
        } else {
            if ($method->isManual() && $state === PaymentStates::STATE_PENDING) {
                $transitions[] = static::TRANSITION_CANCEL;
            }
        }

        return $transitions;
    }

    /**
     * Returns whether the payment can be canceled by the user.
     *
     * @param PaymentInterface $payment
     *
     * @return bool
     */
    static public function isUserCancellable(PaymentInterface $payment)
    {
        return in_array(static::TRANSITION_CANCEL, static::getAvailableTransitions($payment), true);
    }

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