<?php

namespace mii\web;

use Mii;
use mii\core\Component;
use mii\core\ErrorException;
use mii\util\HTML;

class Blocks extends Component
{
    const HEAD = 0;
    const BEGIN = 1;
    const END = 2;

    public $base_path;

    public $use_static = false;

    protected $base_url = '/assets';

    protected $block_class = 'mii\web\Block';

    protected $assets_map_path = '@tmp';

    protected $current_set;

    protected $sets = [];

    protected $_blocks = [];

    protected $_used_blocks = [];
    protected $_used_files = [];

    protected $libraries;

    protected $merge = true;

    protected $use_symlink = true;

    protected $_rendered = false;

    protected $_css = [];

    protected $_js = [[], [], []];

    protected $_files = [
        'css' => [
        ],
        'js' => [
        ],
    ];

    protected $assets;

    public function init(array $config = []): void {
        parent::init($config);

        if (empty($this->sets)) {
            $this->sets[] = [
                'libraries' => [
                    '@app/blocks'
                ],
                'base_url' => '/assets'
            ];
        }

        $this->load_set(key($this->sets));
    }

    public function load_set(string $setname): void {

        $this->current_set = $setname;

        $default_set = [
            'libraries' => [],
            'base_url' => $this->base_url,
            'base_path' => null
        ];

        if (isset($this->sets[$setname])) {
            $set = $this->sets[$setname];
        } else {
            throw new ErrorException("Unknow blocks set name: $setname");
        }

        $set = array_replace($default_set, $set);

        foreach ($set as $key => $value)
            $this->$key = $value;

        for ($i = 0; $i < count($this->libraries); $i++)
            $this->libraries[$i] = Mii::resolve($this->libraries[$i]);
    }

    /**
     * Create a new block, or get an existing block
     * @static
     * @param $name string Block name
     * @return Block
     */
    public function get(string $name): Block {
        if (isset($this->_blocks[$name]))
            return $this->_blocks[$name];

        $this->_blocks[$name] = new $this->block_class($name);

        return $this->_blocks[$name];
    }

    public function get_block_php_file(string $name): ?string {

        $block_file = null;
        $block_path = $this->get_block_path($name);

        if (strpos($name, 'i_') !== 0) {

            $block_path .= $name;

            foreach ($this->libraries as $library_path) {
                if (is_file($library_path . $block_path . '.php')) {
                    return $library_path . $block_path . '.php';
                }
            }
        }
        return null;
    }

    public function get_block_path(string $name): ?string {
        return '/' . implode('/', explode('_', $name)) . '/';
    }

    public function css(): string {
        if (!$this->_rendered)
            $this->render();

        return implode("\n", $this->_css);
    }

    public function js(int $position = null): string {
        if (!$this->_rendered)
            $this->render();

        if ($position === null) {
            $out = [];
            foreach ($this->_js as $js) {
                if (!empty($js))
                    $out[] = implode("\n", $js);
            }
            return implode("\n", $out);
        }

        return implode("\n", $this->_js[$position]);
    }

    public function render(): void {

        if ($this->use_static) {
            $this->static_render();
            return;
        }

        if ($this->base_path === null) {
            $this->base_path = '@pub' . $this->base_url;
        }
        $this->base_path = Mii::resolve($this->base_path);

        if (!is_dir($this->base_path))
            mkdir($this->base_path, 0777, true);

        foreach ($this->_blocks as $block_name => $block) {
            if ($block->__has_parent)
                continue;

            $this->process_block_assets($block_name, $block_name, $block->_depends);
        }

        foreach ($this->_files as $type => $blocks) {
            foreach ($blocks as $block_name => $block) {

                if (isset($block['files']))
                    $this->_build_block($block_name, $type, $block['files']);

                if (isset($block['remote'])) {

                    if ($type === 'js') {
                        foreach ($block['remote'] as $position => $remote) {
                            $this->_js[$position][] = implode("\n", $remote);
                        }
                    } else {

                        foreach ($block['remote'] as $css_remote) {
                            $this->_css[] = '<link type="text/css" href="' . $css_remote . '" rel="stylesheet" />';
                        }
                    }

                }
                if (isset($block['inline'])) {

                    if ($type === 'js') {
                        foreach ($block['inline'] as $position => $inline) {
                            $this->_js[$position][] = '<script type="text/javascript">' . implode("\n", $inline) . '</script>';
                        }

                    } else {
                        $content = implode("\n", $block['inline']);
                        $this->_css[] = '<style>' . $content . '</style>';
                    }
                }

            }
        }

        $this->_rendered = true;
    }

    /**
     * Recursively process a block and its dependencies
     *
     * @param $block_name
     */
    public function process_block_assets($block_name, $parent_block, $depends): void {

        if (isset($this->_used_blocks[$block_name]))
            return;

        $block_path = $this->get_block_path($block_name);

        foreach ($this->libraries as $base_path) {

            if (is_dir($base_path . $block_path . 'assets')) {
                $this->_build_assets_dir($block_name, $base_path . $block_path . 'assets');
                break;
            }
        }

        if (!empty($depends)) {
            foreach ($depends as $depend) {
                if (isset($this->_used_blocks[$depend])) {
                    continue;
                }
                $this->process_block_assets($depend, $parent_block, $this->_blocks[$depend]->_depends);
                $this->_used_blocks[$depend] = true;
            }
        }
        $types = ['css', 'js'];

        foreach ($types as $type) {
            foreach ($this->libraries as $base_path) {

                if (is_file($base_path . $block_path . $block_name . '.' . $type)) {
                    $this->_files[$type][$parent_block]['files'][$block_name . '.' . $type] = $base_path . $block_path . $block_name . '.' . $type;
                    break;
                }
            }
        }

        if ($this->_blocks[$block_name]->__remote_js !== null) {
            foreach ($this->_blocks[$block_name]->__remote_js as $link => $settings) {
                if (!empty($settings) AND isset($settings['position'])) {
                    $position = $settings['position'];
                    unset($settings['position']);
                } else {
                    $position = Blocks::END;;
                }
                $this->_files['js'][$parent_block]['remote'][$position][] = HTML::script($link, $settings);
            }
        }

        if ($this->_blocks[$block_name]->__remote_css !== null) {
            if (!isset($this->_files['css'][$parent_block]['remote']))
                $this->_files['css'][$parent_block]['remote'] = [];

            foreach ($this->_blocks[$block_name]->__remote_css as $link) {
                $this->_files['css'][$parent_block]['remote'][] = $link;
            }
        }

        if (!empty($this->_blocks[$block_name]->__inline_js)) {

            foreach ($this->_blocks[$block_name]->__inline_js as $inline) {
                $position = (!empty($inline[1]) AND isset($inline[1]['position'])) ? $inline[1]['position'] : Blocks::END;
                if (!isset($this->_files['js'][$parent_block]['inline'][$position]))
                    $this->_files['js'][$parent_block]['inline'][$position] = [];
                $this->_files['js'][$parent_block]['inline'][$position][] = $inline[0];
            }
        }

        if (!empty($this->_blocks[$block_name]->__inline_css)) {
            if (!isset($this->_files['css'][$parent_block]['inline']))
                $this->_files['css'][$parent_block]['inline'] = $this->_blocks[$block_name]->__inline_css;
            else
                $this->_files['css'][$parent_block]['inline'] = array_merge($this->_files['.css'][$parent_block]['inline'], $this->_blocks[$block_name]->__inline_css);
        }
    }

    public function assets_path_by_name($block_name) {
        $block_path = $this->get_block_path($block_name);

        foreach ($this->libraries as $base_path) {

            if (is_dir($base_path . $block_path . 'assets')) {
                return $base_path . $block_path . 'assets';
            }
        }
        return false;
    }

    private function _build_block(string $block_name, string $type, array $files): void {

        $result_file_name = $block_name . '.' . substr(md5(implode('', array_keys($files))), 0, 16);

        if ($this->merge) {
            $web_output = $this->base_url . '/' . $result_file_name . '.' . $type;
            $output = $this->base_path . '/' . $result_file_name . '.' . $type;

            $need_recompile = false;

            foreach ($files as $name => $file) {
                if (!is_file($output) || filemtime($output) < filemtime($file)) {
                    $need_recompile = true;
                    break;
                }
            }

            if ($need_recompile) {
                $tmp = '';
                foreach ($files as $file) {
                    $tmp .= file_get_contents($file) . "\n";
                }

                file_put_contents($output, $tmp);
            }

            if ($type === 'css') {
                $this->_css[] = '<link type="text/css" href="' . $web_output . '?' . filemtime($output) . '" rel="stylesheet">';

            } else {
                $this->_js[Blocks::END][] = '<script src="' . $web_output . '?' . filemtime($output) . '"></script>';
            }

            return;
        }

        foreach ($files as $name => $file) {

            $output = $this->base_path . '/' . $name;

            if (!is_file($output) || filemtime($output) < filemtime($file)) {

                try {
                    copy($file, $output);
                } catch (\Exception $e) {
                    $dir = dirname($output);
                    mkdir($dir, 0777, true);
                    copy($file['path'], $output);
                }
            }
            if ($type === 'css') {
                $this->_css[] = '<link type="text/css" href="' . $this->base_url . '/' . $name . '?' . filemtime($output) . '" rel="stylesheet">';
            } else {
                $this->_js[Blocks::END][] = '<script src="' . $this->base_url . '/' . $name . '?' . filemtime($output) . '"></script>';
            }
        }
    }

    private function _build_assets_dir(string $blockname, string $path): void {

        $output = $this->base_path . '/' . $blockname;

        if (true || $this->use_symlink) {
            if (!is_link($output)) {
                symlink($path, $output);
            }
        } else {
            $this->_copy_dir($path, $output);
        }

    }

    // todo:
    private function _copy_dir($from, $to) {

        $dir = opendir($from);
        @mkdir($to);
        while (false !== ($file = readdir($dir))) {
            if (($file !== '.') && ($file !== '..')) {
                if (is_dir($from . '/' . $file)) {
                    $this->_copy_dir($from . '/' . $file, $to . '/' . $file);
                } else {

                    if (static::is_modified_later($to . '/' . $file, filemtime($from . '/' . $file))) {
                        copy($from . '/' . $file, $to . '/' . $file);
                    }
                }
            }
        }
        closedir($dir);
    }

    private function static_render() {

        $this->assets_map_path = Mii::resolve($this->assets_map_path);
        $this->assets = require($this->assets_map_path . "/{$this->current_set}.assets");

        foreach ($this->_blocks as $block_name => $block) {
            if ($block->__has_parent)
                continue;
            $this->static_process_block_assets($block_name, $block_name, $block->_depends);
        }

        $this->_rendered = true;
    }

    /**
     * Recursively process a block and its dependencies
     *
     * @param $block_name
     */
    public function static_process_block_assets($block_name, $parent_block, $depends): void {
        if (isset($this->_used_blocks[$block_name])) {
            return;
        }

        if (!empty($depends)) {
            foreach ($depends as $depend) {
                $this->static_process_block_assets($depend, $parent_block, $this->_blocks[$depend]->_depends);
                $this->_used_blocks[$depend] = true;
            }
        }

        if (isset($this->assets['css'][$block_name])) {
            $filename = $this->assets['css'][$block_name];
            if (!isset($this->_used_files['css'][$filename])) {
                $this->_css[] = '<link type="text/css" href="' . $this->base_url . '/' . $filename . '.css" rel="stylesheet">';
                $this->_used_files['css'][$filename] = true;
            }
        }

        if (isset($this->assets['js'][$block_name])) {
            $filename = $this->assets['js'][$block_name];
            if (!isset($this->_used_files['js'][$filename])) {
                $this->_js[Blocks::END][] = '<script src="' . $this->base_url . '/' . $filename . '.js"></script>';
                $this->_used_files['js'][$filename] = true;
            }
        }

        if ($this->_blocks[$block_name]->__remote_js !== null) {
            foreach ($this->_blocks[$block_name]->__remote_js as $link => $settings) {
                if (!empty($settings) AND isset($settings['position'])) {
                    $position = $settings['position'];
                    unset($settings['position']);
                } else {
                    $position = Blocks::END;
                }
                $this->_js[$position][] = HTML::script($link, $settings);
            }
        }

        if ($this->_blocks[$block_name]->__remote_css !== null) {
            foreach ($this->_blocks[$block_name]->__remote_css as $link) {
                $this->_css[] = '<link type="text/css" href="' . $link . '" rel="stylesheet" />';
            }
        }

        if (!empty($this->_blocks[$block_name]->__inline_js)) {

            foreach ($this->_blocks[$block_name]->__inline_js as $inline) {
                $position = (!empty($inline[1]) AND isset($inline[1]['position'])) ? $inline[1]['position'] : Blocks::END;
                $this->_js[$position][] = '<script type="text/javascript">' . $inline[0] . '</script>';
            }
        }

        if (!empty($this->_blocks[$block_name]->__inline_css)) {
            foreach ($this->_blocks[$block_name]->__inline_css as $style) {
                $this->_css[] = HTML::tag('style', $style[0], $style[1]);
            }
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Helper;

use Doctrine\ORM\EntityManagerInterface;
use Ekyna\Bundle\AdminBundle\Helper\ResourceHelper;
use Ekyna\Bundle\CommerceBundle\Service\ConstantsHelper;
use Ekyna\Component\Commerce\Common\Util\FormatterAwareTrait;
use Ekyna\Component\Commerce\Common\Util\FormatterFactory;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectInterface;
use Ekyna\Component\Commerce\Stock\Model\StockUnitInterface;
use Ekyna\Component\Commerce\Stock\Model\StockUnitStates;
use Ekyna\Component\Commerce\Stock\Repository\StockUnitRepositoryInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

/**
 * Class SubjectNormalizerHelper
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Helper
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SubjectNormalizerHelper
{
    use NormalizerAwareTrait,
        FormatterAwareTrait;

    /**
     * @var ConstantsHelper
     */
    protected $constantHelper;

    /**
     * @var ResourceHelper
     */
    protected $resourceHelper;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * Constructor.
     *
     * @param FormatterFactory       $formatterFactory
     * @param ConstantsHelper        $constantHelper
     * @param ResourceHelper         $resourceHelper
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(
        FormatterFactory $formatterFactory,
        ConstantsHelper $constantHelper,
        ResourceHelper $resourceHelper,
        EntityManagerInterface $entityManager
    ) {
        $this->formatterFactory = $formatterFactory;
        $this->constantHelper = $constantHelper;
        $this->resourceHelper = $resourceHelper;
        $this->entityManager = $entityManager;
    }

    /**
     * Normalize the subject's stock data.
     *
     * @param StockSubjectInterface $subject
     * @param string                $format
     * @param array                 $context
     *
     * @return array
     */
    public function normalizeStock(StockSubjectInterface $subject, $format = null, array $context = [])
    {
        $translator = $this->constantHelper->getTranslator();
        $formatter = $this->getFormatter();

        if (null !== $eda = $subject->getEstimatedDateOfArrival()) {
            $eda = $formatter->date($eda);
        } else {
            $eda = $translator->trans('ekyna_core.value.undefined');
        }

        $stockUnits = $this->findStockUnits($subject);

        return [
            'mode_label'    => $this->constantHelper->renderStockSubjectModeLabel($subject),
            'mode_badge'    => $this->constantHelper->renderStockSubjectModeBadge($subject),
            'state_label'   => $this->constantHelper->renderStockSubjectStateLabel($subject),
            'state_badge'   => $this->constantHelper->renderStockSubjectStateBadge($subject),
            'in'            => $formatter->number($subject->getInStock()),
            'available'     => $formatter->number($subject->getAvailableStock()),
            'virtual'       => $formatter->number($subject->getVirtualStock()),
            'floor'         => $formatter->number($subject->getStockFloor()),
            'geocode'       => $subject->getGeocode(),
            'replenishment' => $formatter->number($subject->getReplenishmentTime()),
            'eda'           => $eda,
            'moq'           => $formatter->number($subject->getMinimumOrderQuantity()),
            'quote_only'    => $subject->isQuoteOnly()
                ? $translator->trans('ekyna_core.value.yes')
                : $translator->trans('ekyna_core.value.no'),
            'end_of_life'   => $subject->isEndOfLife()
                ? $translator->trans('ekyna_core.value.yes')
                : $translator->trans('ekyna_core.value.no'),
            'stock_units'   => $this->normalizer->normalize($stockUnits, $format, $context),
        ];
    }

    /**
     * Sorts the stock units.
     *
     * @param StockSubjectInterface $subject
     *
     * @return StockUnitInterface[]
     */
    private function findStockUnits(StockSubjectInterface $subject)
    {
        /** @var StockUnitRepositoryInterface $repository */
        $repository = $this->entityManager->getRepository($subject::getStockUnitClass());

        /** @var StockUnitInterface[] $stockUnits */
        $stockUnits = array_merge(
            $repository->findNotClosedBySubject($subject),
            $repository->findLatestClosedBySubject($subject)
        );

        // Sort by "created/closed at" date desc
        usort($stockUnits, function (StockUnitInterface $a, StockUnitInterface $b) {
            if ($a->getState() === StockUnitStates::STATE_CLOSED && $b->getState() !== StockUnitStates::STATE_CLOSED) {
                return 1;
            }

            if ($a->getState() !== StockUnitStates::STATE_CLOSED && $b->getState() === StockUnitStates::STATE_CLOSED) {
                return -1;
            }

            if ($a->getState() === StockUnitStates::STATE_CLOSED && $b->getState() === StockUnitStates::STATE_CLOSED) {
                $aDate = $a->getClosedAt()->getTimestamp();
                $bDate = $b->getClosedAt()->getTimestamp();

                if ($aDate > $bDate) {
                    return -1;
                }

                if ($aDate < $bDate) {
                    return 1;
                }
            }

            $aDate = $a->getCreatedAt()->getTimestamp();
            $bDate = $b->getCreatedAt()->getTimestamp();

            if ($aDate > $bDate) {
                return -1;
            }

            if ($aDate < $bDate) {
                return 1;
            }

            return 0;
        });

        return $stockUnits;
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Prioritizer;

use Ekyna\Component\Commerce\Stock\Model\StockAssignmentInterface;
use Ekyna\Component\Commerce\Stock\Resolver\StockUnitResolverInterface;

/**
 * Class PrioritizeHelper
 * @package Ekyna\Component\Commerce\Stock\Prioritizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 * @TODO rename to PrioritizeUnitGuesser ? or PrioritizeUnitResolver ?
 */
class PrioritizeHelper
{
    /**
     * @var StockUnitResolverInterface
     */
    private $unitResolver;

    /**
     * Constructor.
     *
     * @param StockUnitResolverInterface $unitResolver
     */
    public function __construct(StockUnitResolverInterface $unitResolver)
    {
        $this->unitResolver = $unitResolver;
    }

    /**
     * Finds the best stock unit to move/merge assignment(s) into for the given quantity.
     *
     * @param StockAssignmentInterface $assignment
     * @param float                    $quantity
     *
     * @return UnitCandidate[]
     */
    public function getUnitCandidates(StockAssignmentInterface $assignment, $quantity)
    {
        $subject = $assignment->getStockUnit()->getSubject();

        // Find the subject's ready stock units
        if (empty($units = $this->unitResolver->findReady($subject))) {
            return [];
        };

        $sale = $assignment->getSaleItem()->getSale();

        $candidates = [];

        foreach ($units as $unit) {
            $this->unitResolver->getStockUnitCache()->add($unit);

            $candidate = UnitCandidate::build($unit, $sale);

            // Skip if no reservable and no releasable quantity
            if ((0 >= $candidate->reservable) && (0 >= $candidate->releasable)) {
                continue;
            }

            $add = false;
            $diff = 0;

            if (0 < $candidate->reservable) {
                // Unit has enough reservable quantity
                if (empty($candidates) && 0 >= $diff = $quantity - $candidate->reservable) {
                    return [$candidate];
                }

                $add = true;
            }

            if (null !== $candidate->getCombination(max($diff, $candidate->releasable))) {
                $add = true;
            }

            if ($add) {
                $candidates[] = $candidate;
            }
        }

        if (empty($candidates)) {
            return [];
        }

        // Sort candidates
        usort($candidates, function (UnitCandidate $a, UnitCandidate $b) use ($quantity) {
            // TODO sort by (reservable + releasable) DESC

            // TODO Simplify. Prefer:
            // - 'shippable' enough
            // - 'reservable' eq/positive/closest
            // - 'releasable' eq/positive/closest

            // Prefer units with shippable >= quantity, reservable >= quantity or releasable >= quantity
            foreach (['shippable', 'reservable', 'releasable'] as $property) {
                if (false !== $r = $this->ceilComparison($a, $b, $property, $quantity)) {
                    return $r;
                }
            }
            // Prefer units with shippable == quantity, reservable == quantity or releasable == quantity
            foreach (['reservable', 'releasable'] as $property) { // 'shippable' ?
                if (false !== $r = $this->equalComparison($a, $b, $property, $quantity)) {
                    return $r;
                }
            }

            // TODO (temporary)
            foreach (['shippable', 'reservable', 'releasable'] as $property) {
                if ($a->{$property} != $b->{$property}) {
                    return $b->{$property} - $a->{$property};
                }
            }
            return 0;

            // Prefer units with assignments combination's releasable quantity (sum) that
            // equals or is greater than (prefer closest) aimed quantity.
            if ($a->combination->sum == $b->combination->sum) {
                return 0;
            }
            if (0 <= $a->combination->sum) {
                return intval(0 > $b->combination->sum ? -1 : $a->combination->sum - $b->combination->sum);
            }

            return intval(0 < $b->combination->sum ? 1 : $b->combination->sum - $a->combination->sum);
        });

        return $candidates;
    }

    /**
     * Returns -1 if A's property is greater than or equal quantity but not B.
     * Returns 1 if B's property is greater than or equal quantity but not A.
     * Else returns false.
     *
     * @param UnitCandidate $a
     * @param UnitCandidate $b
     * @param string        $property
     * @param float         $quantity
     *
     * @return bool|int
     */
    private function ceilComparison(UnitCandidate $a, UnitCandidate $b, $property, $quantity)
    {
        if ($a->{$property} >= $quantity && $b->{$property} < $quantity) {
            return -1;
        }
        if ($a->{$property} < $quantity && $b->{$property} >= $quantity) {
            return 1;
        }

        return false;
    }

    /**
     * Returns -1 if A's equals quantity but not B.
     * Returns 1 if B's equals quantity but not A.
     * Else returns false.
     *
     * @param UnitCandidate $a
     * @param UnitCandidate $b
     * @param string        $property
     * @param float         $quantity
     *
     * @return bool|int
     */
    private function equalComparison(UnitCandidate $a, UnitCandidate $b, $property, $quantity)
    {
        if ($a->{$property} == $quantity && $b->{$property} != $quantity) {
            return -1;
        }
        if ($a->{$property} != $quantity && $b->{$property} == $quantity) {
            return 1;
        }

        return false;
    }

//    /**
//     * Returns -1 if A's property is greater than
//     *
//     * @param UnitCandidate $a
//     * @param UnitCandidate $b
//     * @param string        $property
//     *
//     * @return bool|int
//     */
//    private function greaterThanComparison(UnitCandidate $a, UnitCandidate $b, $property)
//    {
//        if ($a->{$property} > $b->{$property}) {
//            return -1;
//        }
//        if ($a->{$property} < $b->{$property}) {
//            return 1;
//        }
//
//        return false;
//    }
}


<?php

namespace Ekyna\Component\Commerce\Document\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\Quote\Model\QuoteInterface;

/**
 * Class DocumentTypes
 * @package Ekyna\Component\Commerce\Document\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class DocumentTypes
{
    const TYPE_FORM         = 'form';
    const TYPE_VOUCHER      = 'voucher';
    const TYPE_QUOTE        = 'quote';
    const TYPE_PROFORMA     = 'proforma';
    const TYPE_CONFIRMATION = 'confirmation';

    /**
     * Returns the invoice types.
     *
     * @return array
     */
    static public function getTypes()
    {
        return [
            static::TYPE_FORM,
            static::TYPE_VOUCHER,
            static::TYPE_QUOTE,
            static::TYPE_PROFORMA,
            static::TYPE_CONFIRMATION,
        ];
    }

    /**
     * Returns the sale classes supported by the given type for document generation.
     *
     * @param string $type
     *
     * @return array
     */
    static public function getClasses($type)
    {
        switch ($type) {
            case static::TYPE_FORM:
                return [CartInterface::class];
            case static::TYPE_QUOTE:
                return [QuoteInterface::class];
            case static::TYPE_PROFORMA:
                return [QuoteInterface::class, OrderInterface::class];
            case static::TYPE_CONFIRMATION:
                return [OrderInterface::class];
            case static::TYPE_VOUCHER:
                return [];
            default:
                throw new InvalidArgumentException("Unexpected type '$type'.");
        }
    }

    /**
     * Returns whether the given type is valid or not.
     *
     * @param string $type
     *
     * @return bool
     */
    static public function isValidType($type)
    {
        return in_array($type, static::getTypes(), true);
    }

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


<?php

namespace mii\util;

class URL
{

    /**
     * Gets the base URL to the application.
     * To specify a protocol, provide the protocol as a string or request object.
     * If a protocol is used, a complete URL will be generated using the
     * `$_SERVER['HTTP_HOST']` variable.
     *
     *     // Absolute URL path with no host or protocol
     *     echo URL::base();
     *
     *     // Absolute URL path with host, https protocol
     *     echo URL::base('https');
     *
     *     // Absolute URL path with '//'
     *     echo URL::base('//');
     *
     * @param   mixed $protocol Protocol string or boolean
     * @return  string
     */
    public static function base($protocol = null): string {
        if ($protocol === null) {
            return \Mii::$app->base_url;
        }

        if ($protocol === TRUE) {
            return \Mii::$app->request->get_hostname() . \Mii::$app->base_url;
        }

        if ($protocol !== '//')
            $protocol = $protocol . '://';

        $domain = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME'];

        return $protocol . $domain . \Mii::$app->base_url;
    }

    /**
     * Fetches an absolute site URL based on a URI segment.
     *
     *     echo URL::site('foo/bar');
     *
     * @param   string $uri Site URI to convert
     * @param   mixed $protocol Protocol string or true
     * @return  string
     */
    public static function site(string $uri = '', $protocol = null): string {
        // Chop off possible scheme, host, port, user and pass parts
        $path = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', trim($uri, '/'));

        if (preg_match('/[^\x00-\x7F]/S', $path)) {
            // Encode all non-ASCII characters, as per RFC 1738
            $path = preg_replace_callback('~([^/]+)~', '\mii\util\URL::_rawurlencode_callback', $path);
        }

        // Concat the URL
        return URL::base($protocol) . $path;
    }

    /**
     * Callback used for encoding all non-ASCII characters, as per RFC 1738
     * Used by URL::site()
     *
     * @param  array $matches Array of matches from preg_replace_callback()
     * @return string          Encoded string
     */
    protected static function _rawurlencode_callback($matches) {
        return rawurlencode($matches[0]);
    }

    /**
     * Merges the current GET parameters with an array of new or overloaded
     * parameters and returns the resulting query string.
     *
     *     // Returns "?sort=title&limit=10" combined with any existing GET values
     *     $query = URL::query(array('sort' => 'title', 'limit' => 10));
     *
     * Typically you would use this when you are sorting query results,
     * or something similar.
     *
     * [!!] Parameters with a NULL value are left out.
     *
     * @param   array $params Array of GET parameters
     * @param   boolean $use_get Include current request GET parameters
     * @return  string
     */
    public static function query(array $params = null, $use_get = null) {
        if ($use_get) {
            if ($params === NULL) {
                // Use only the current parameters
                $params = $_GET;
            } else {
                // Merge the current and new parameters
                $params = Arr::merge($_GET, $params);
            }
        }

        if (empty($params)) {
            // No query parameters
            return '';
        }

        // Note: http_build_query returns an empty string for a params array with only NULL values
        $query = http_build_query($params, '', '&');

        // Don't prepend '?' to an empty string
        return ($query === '') ? '' : ('?' . $query);
    }

    /**
     * Convert a phrase to a URL-safe title.
     *
     *     echo URL::title('My Blog Post'); // "my-blog-post"
     *
     * @param   string $title Phrase to convert
     * @param   string $separator Word separator (any single character)
     * @param   boolean $ascii_only Transliterate to ASCII?
     * @return  string
     * @uses    UTF8::transliterate_to_ascii
     */
    public static function title($title, $separator = '-', $ascii_only = FALSE) {
        if ($ascii_only === TRUE) {
            // Transliterate non-ASCII characters
            $title = UTF8::transliterate_to_ascii($title);

            // Remove all characters that are not the separator, a-z, 0-9, or whitespace
            $title = preg_replace('![^' . preg_quote($separator) . 'a-z0-9\s]+!', '', strtolower($title));
        } else {
            // Remove all characters that are not the separator, letters, numbers, or whitespace
            $title = preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', UTF8::strtolower($title));
        }

        // Replace all separator characters and whitespace by a single separator
        $title = preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title);

        // Trim separators from the beginning and end
        return trim($title, $separator);
    }

    static public function current(array $params = null): string {
        return static::site(\Mii::$app->request->uri()) . static::query($params, true);
    }

    static public function back_url(string $default = null): string {
        if (isset($_GET['back_url']))
            return urldecode($_GET['back_url']);

        if ($default === null) {
            return URL::current();
        }

        return $default;
    }

}


<?php

namespace Ekyna\Component\Commerce\Common\Generator;

/**
 * Class DateNumberGenerator
 * @package Ekyna\Component\Commerce\Common\Generator
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class DateNumberGenerator extends DefaultNumberGenerator
{
    /**
     * Generates the number.
     *
     * @param string $number
     *
     * @return string
     */
    protected function generateNumber($number)
    {
        $datePrefix = (new \DateTime())->format($this->prefix);

        if (0 !== strpos($number, $datePrefix)) {
            $number = 0;
        } else {
            $number = intval(substr($number, strlen($datePrefix)));
        }

        if ($this->debug && 9999 > $number) {
            $number = 9999;
        }

        return $datePrefix . str_pad($number + 1, $this->length - strlen($datePrefix), '0', STR_PAD_LEFT);
    }
}


<?php

namespace Skriv\Markup\DocBook;

/**
 * SkrivMarkup configuration object.
 * This object is based on the WikiRenderer project created by Laurent Jouanneau.
 *
 * @author	Amaury Bouchard <amaury@amaury.net>
 * @copyright	© 2013, Amaury Bouchard
 * @package	SkrivMarkup
 * @see		WikiRenderer
 */
class Config extends \WikiRenderer\Config  {
	/** ??? */
	public $defaultTextLineContainer = '\WikiRenderer\HtmlTextLine';
	/** List of inline markups. */
	public $textLineContainers = array(
		'\WikiRenderer\HtmlTextLine' => array(
			'\Skriv\Markup\DocBook\Strong',		// **strong**
			'\Skriv\Markup\DocBook\Em',		// ''em''
			'\Skriv\Markup\DocBook\Strikeout',	// --strikeout--
			'\Skriv\Markup\DocBook\Underline',	// __underline__
			'\Skriv\Markup\DocBook\Monospace',	// ##monospace##
			'\Skriv\Markup\DocBook\Superscript',	// ^^superscript^^
			'\Skriv\Markup\DocBook\Subscript',	// ,,subscript,,
			'\Skriv\Markup\DocBook\Abbr',		// ??abbr|text??
			'\Skriv\Markup\DocBook\Link',		// [[link|url]]		[[url]]
			'\Skriv\Markup\DocBook\Image',		// {{image|url}}	{{url}}
			'\Skriv\Markup\DocBook\Footnote',	// ((footnote))		((label|footnote))
			'\Skriv\Markup\DocBook\Anchor',		// ~~anchor~~
		)
	);
	/** List of bloc markups. */
	public $blocktags = array(
		'\Skriv\Markup\DocBook\Title',
		'\Skriv\Markup\DocBook\WikiList',
		'\Skriv\Markup\DocBook\Code',
		'\Skriv\Markup\DocBook\Pre',
		'\Skriv\Markup\DocBook\Hr',
		'\Skriv\Markup\DocBook\Blockquote',
		'\Skriv\Markup\DocBook\Table',
		'\Skriv\Markup\DocBook\StyledBlock',
		'\Skriv\Markup\DocBook\Paragraph',
	);

	/* ************ SKRIV MARKUP SPECIFIC ATTRIBUTES ************* */
	/** List of the footnotes. */
	private $_footnotes = null;
	/** Tree representing the table of content. */
	private $_toc = null;
	/** Parent configuration object, used for recursive calls. */
	private $_parentConfig = null;
	/** Hash containing the configuration parameters. */
	private $_params = null;
	/** ??? */
	private $_sectionLevel= array();

	/* ******************** CONSTRUCTION ****************** */
	/**
	 * Constructor.
	 * @param	array	$param		(optionnel) Hash containing specific configuration parameters.
	 *		- bool		shortenLongUrl		Specifies if we must shorten URLs longer than 40 characters. (default: true)
	 *		- bool		convertSmileys		Specifies if we must convert smileys. (default: true)
	 *		- bool		convertSymbols		Specifies if we must convert symbols. (default: true)
	 *		- Closure	urlProcessFunction	URLs processing function. (default: null)
	 *		- Closure	preParseFunction	Function for pre-parse process. (default: null)
	 *		- Closure	postParseFunction	Function for post-parse process. (default: null)
	 *		- Closure	titleToIdFunction	Function that converts title strings into HTML identifiers. (default: null)
	 *		- string	anchorsPrefix		Prefix of anchors' identifiers. (default: "skriv-" + random value)
	 *		- string	footnotesPrefix		Prefix of footnotes' identifiers. (default: "skriv-notes-" + random value)
	 *		- bool		codeSyntaxHighlight	Activate code highlighting. (default: true)
	 *		- bool		codeLineNumbers		Line numbers in code blocks. (default: true)
	 *		- int		firstTitleLevel		Offset of first level titles. (default: 1)
	 *		- bool		targetBlank		Add "target='_blank'" to every links.
	 *		- bool		nofollow		Add "rel='nofollow'" to every links.
	 *		- bool		addFootnotes		Add footnotes' content at the end of the page.
	 *		- bool		codeInlineStyles	Activate inline styles in code blocks. (default: false)
	 * @param	\Skriv\Markup\Html\Config	parentConfig	Parent configuration object, for recursive calls.
	 */
	public function __construct(array $param=null, \Skriv\Markup\Html\Config $parentConfig=null) {
		// creation of the default parameters array
		$randomId = base_convert(rand(0, 50000), 10, 36);
		$this->_params = array(
			'shortenLongUrl'	=> true,
			'convertSmileys'	=> true,
			'convertSymbols'	=> true,
			'anchorsPrefix'		=> '',
			'footnotesPrefix'	=> "skriv-notes-$randomId-",
			'urlProcessFunction'	=> null,
			'preParseFunction'	=> null,
			'postParseFunction'	=> null,
			'titleToIdFunction'	=> null,
			'codeSyntaxHighlight'	=> true,
			'codeLineNumbers'	=> true,
			'firstTitleLevel'	=> 1,
			'targetBlank'		=> null,
			'nofollow'		=> null,
			'addFootnotes'		=> false,
			'codeInlineStyles'	=> false
		);
		// processing of specified parameters
		if (isset($param['shortenLongUrl']) && $param['shortenLongUrl'] === false)
			$this->_params['shortenLongUrl'] = false;
		if (isset($param['convertSmileys']) && $param['convertSmileys'] === false)
			$this->_params['convertSmileys'] = false;
		if (isset($param['convertSymbols']) && $param['convertSymbols'] === false)
			$this->_params['convertymbols'] = false;
		if (isset($param['anchorsPrefix']))
			$this->_params['anchorsPrefix'] = $param['anchorsPrefix'];
		if (isset($param['footnotesPrefix']))
			$this->_params['footnotesPrefix'] = $param['footnotesPrefix'];
		if (isset($param['urlProcessFunction']) && is_a($param['urlProcessFunction'], 'Closure'))
			$this->_params['urlProcessFunction'] = $param['urlProcessFunction'];
		if (isset($param['preParseFunction']) && is_a($param['preParseFunction'], 'Closure'))
			$this->_params['preParseFunction'] = $param['preParseFunction'];
		if (isset($param['postParseFunction']) && is_a($param['postParseFunction'], 'Closure'))
			$this->_params['postParseFunction'] = $param['postParseFunction'];
		if (isset($param['titleToIdFunction']) && is_a($param['titleToIdFunction'], 'Closure'))
			$this->_params['titleToIdFunction'] = $param['titleToIdFunction'];
		if (isset($param['codeSyntaxHighlight']) && $param['codeSyntaxHighlight'] === false)
			$this->_params['codeSyntaxHighlight'] = $param['codeSyntaxHighlight'];
		if (isset($param['codeLineNumbers']) && $param['codeLineNumbers'] === false)
			$this->_params['codeLineNumbers'] = false;
		if (isset($param['firstTitleLevel']) && is_numeric($param['firstTitleLevel']) &&
		    $param['firstTitleLevel'] >= 1 && $param['firstTitleLevel'] <= 6)
			$this->_params['firstTitleLevel'] = $param['firstTitleLevel'];
		if (isset($param['targetBlank']) && is_bool($param['targetBlank']))
			$this->_params['targetBlank'] = $param['targetBlank'];
		if (isset($param['nofollow']) && is_bool($param['nofollow']))
			$this->_params['nofollow'] = $param['nofollow'];
		if (isset($param['addFootnotes']) && $param['addFootnotes'] === true)
			$this->_params['addFootnotes'] = $param['addFootnotes'];
		if (isset($param['codeInlineStyles']) && $param['codeInlineStyles'] === true)
			$this->_params['codeInlineStyles'] = $param['codeInlineStyles'];
		// storing the parent configuration object
		$this->_parentConfig = $parentConfig;
		// footnotes liste init
		$this->_footnotes = array();
	}
	/**
	 * Build an object of the same type, "child" of the current object.
	 * @return	\Skriv\Markup\\Html\Config	The new configuration object.
	 */
	public function subConstruct() {
		return (new Config($this->_params, $this));
	}

	/* *************** PARAMETERS MANAGEMENT ************* */
	/**
	 * Returns a specific configuration parameter. If a parent configuration object exists, the parameter is asked to it.
	 * @param	string	$param	Parameter's name.
	 * @return	mixed	Value of the configuration parameter.
	 */
	public function getParam($param) {
		if (isset($this->_parentConfig))
			return ($this->_parentConfig->getParam($param));
		return (isset($this->_params[$param]) ? $this->_params[$param] : null);
	}

	/* *************** TEXT MANAGEMENT ************* */
	/**
	 * Convert title string to a usable HTML identifier.
	 * @param	int	$depth	Depth of the title.
	 * @param	string	$text	Input string.
	 * @return	string	The converted string.
	 */
	public function titleToIdentifier($depth, $text) {
		$func = $this->getParam('titleToIdFunction');
		if (isset($func))
			return ($func($depth, $text));
		// conversion of accented characters
		// see http://www.weirdog.com/blog/php/supprimer-les-accents-des-caracteres-accentues.html
		$text = htmlentities($text, ENT_NOQUOTES, 'utf-8');
		$text = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $text);
		$text = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $text);	// for ligatures e.g. 'œ'
		$text = preg_replace('#&[^;]+;#', '', $text);			// strips other characters

		$text = preg_replace("/[^a-zA-Z0-9_-]/", ' ', $text);		// remove any other characters
		$text = str_replace(' ', '-', $text);
		$text = preg_replace('/\s+/', " ", $text);
		$text = preg_replace('/-+/', "-", $text);
		$text = trim($text, '-');
		$text = trim($text);
		$text = empty($text) ? '-' : $text;

		return ($text);
	}

	/* *************** PARSING MANAGEMENT **************** */
	/**
	 * Method called for pre-parse processing.
	 * @param	string	$text	The input text.
	 * @return	string	The text that will be parsed.
	 */
	public function onStart($text) {
		// ???
		$this->_sectionLevel = array();
		// process of smileys and other special characters
		if ($this->getParam('convertSmileys'))
			$text = Smiley::convertSmileys($text);
		if ($this->getParam('convertSymbols'))
			$text = Smiley::convertSymbols($text);
		// if a specific pre-parse function was defined, it is called
		$func = $this->getParam('preParseFunction');
		if (isset($func))
			$text = $func($text);
		return ($text);
	}
	/**
	 * Method called for post-parse processing.
	 * @param	string	$finalText	The generated text.
	 * @return	string	The text after post-processing.
	 */
	public function onParse($finalText) {
		// if a specific post-parse function was defined, it is called
		$func = $this->getParam('postParseFunction');
		if (isset($func))
			$finalText = $func($finalText);
		// add footnotes' content if needed
		if ($this->getParam('addFootnotes')) {
			$footnotes = $this->getFootnotes();
			if (!empty($footnotes))
				$finalText .= "\n" . $footnotes;
		}
		// ???
		$finalText .= str_repeat('
', count($this->_sectionLevel));
		return ($finalText);
	}
	/**
	 * Links processing.
	 * @param	string	$url		The URL to process.
	 * @param	string	$tagName	Name of the calling tag.
	 * @return	array	Array with the processed URL and the generated label.
	 *			Third parameter is about blank targeting of the link. It could be
	 *			null (use the default behaviour), true (add a blank targeting) or
	 *			false (no blank targeting).
	 */
	public function processLink($url, $tagName='') {
		$label = $url = trim($url);
		$targetBlank = $this->getParam('targetBlank');
		$nofollow = $this->getParam('nofollow');
		// shortening of long URLs
		if ($this->getParam('shortenLongUrl') && strlen($label) > 40)
			$label = substr($label, 0, 40) . '...';
		// Javascript XSS check
		if (substr($url, 0, strlen('javascript:')) === 'javascript:')
			$url = '#';
		else {
			// email check
			if (filter_var($url, FILTER_VALIDATE_EMAIL)) {
				$url = "mailto:$url";
				$targetBlank = $nofollow = false;
			} else if (substr($url, 0, strlen('mailto:')) === 'mailto:') {
				$label = substr($url, strlen('mailto:'));
				$targetBlank = $nofollow = false;
			}
			// if a specific URL process function was defined, it is called
			$func = $this->getParam('urlProcessFunction');
			if (isset($func))
				list($url, $label, $targetBlank, $nofollow) = $func($url, $label, $targetBlank, $nofollow);
		}

		return (array($url, $label, $targetBlank, $nofollow));
	}

	/* ******************** TOC MANAGEMENT *************** */
	/**
	 * Add a TOC entry.
	 * @param	int	$depth		Depth in the tree.
	 * @param	string	$title		Name of the new entry.
	 * @param	string	$identifier	Identifier of the new entry.
	 */
	public function addTocEntry($depth, $title, $identifier) {
		if (!isset($this->_toc))
			$this->_toc = array();
		$this->_addTocSubEntry($depth, $depth, $title, $identifier, $this->_toc);
	}
	/**
	 * Returns the TOC content. By default, the rendered HTML is returned, but the
	 * raw TOC tree is available.
	 * @param	bool	$raw	(optional) Set to True to get the raw TOC tree. False by default.
	 * @return	string|array	The TOC rendered HTML or the TOC tree.
	 */
	public function getToc($raw=false) {
		if ($raw === true)
			return ($this->_toc['sub']);
		$html = $this->_getRenderedToc($this->_toc['sub']);
		return ($html);
	}

	/* ******************** FOOTNOTES MANAGEMENT **************** */
	/**
	 * Add a footnote.
	 * @param	string	$text	Footnote's text.
	 * @param	string	$label	(optionnel) Footnote's label. If not given, an auto-incremented
	 *				number will be used.
	 * @return	array	Hash with 'id' and 'index' keys.
	 */
	public function addFootnote($text, $label=null) {
		if (isset($this->_parentConfig))
			return ($this->_parentConfig->addFootnote($text, $label));
		if (is_null($label))
			$this->_footnotes[] = $text;
		else
			$this->_footnotes[] = array(
				'label'	=> $label,
				'text'	=> $text
			);
		$index = count($this->_footnotes);
		return (array(
			'id'	=> $this->getParam('footnotesPrefix') . "-$index",
			'index'	=> $index
		));
	}
	/**
	 * Returns the footnotes content. By default, the rendered HTML is returned, but the
	 * raw list of footnotes is available.
	 * @param	bool	$raw	(optional) Set to True to get the raw list of footnotes.
	 *				False by default.
	 * @return	string|array	The footnotes' rendered HTML or the list of footnotes.
	 */
	public function getFootnotes($raw=false) {
		if ($raw === true)
			return ($this->_footnotes);
		if (empty($this->_footnotes))
			return (null);
		$footnotes = '';
		$index = 1;
		foreach ($this->_footnotes as $note) {
			$id = $this->getParam('footnotesPrefix') . "-$index";
			$noteHtml = "
";
			if (is_string($note))
				$noteHtml .= "$index. $note";
			else
				$noteHtml .= htmlspecialchars($note['label']) . ". " . $note['text'];
			$noteHtml .= "
\n";
			$footnotes .= $noteHtml;
			$index++;
		}
		$footnotes = "
\n$footnotes
\n";
		return ($footnotes);
	}

	/* ****************** PRIVATE METHODS ******************** */
	/**
	 * Add a sub-TOC entry.
	 * @param	int	$depth		Depth in the tree.
	 * @param	int	$level		Level of the title.
	 * @param	string	$title		Name of the new entry.
	 * @param	string	$identifier	Identifier of the new entry.
	 * @param	array	$list		List of sibbling nodes.
	 */
	private function _addTocSubEntry($depth, $level, $title, $identifier, &$list) {
		if (!isset($list['sub']))
			$list['sub'] = array();
		$offset = count($list['sub']);
		if ($depth === 1) {
			$list['sub'][$offset] = array(
				'id'	=> $identifier,
				'value'	=> $title
			);
			return;
		}
		$offset--;
		if (!isset($list['sub'][$offset]))
			$list['sub'][$offset] = array();
		$this->_addTocSubEntry($depth - 1, $level, $title, $identifier, $list['sub'][$offset]);
	}
	/**
	 * Returns a chunk of rendered TOC.
	 * @param	array	$list	List of TOC entries.
	 * @param	int	$depth	(optional) Depth in the tree. 1 by default.
	 * @return	string	The rendered chunk.
	 */
	private function _getRenderedToc($list, $depth=1) {
		if (!isset($list) || empty($list))
			return ('');
		$html = "
\n";
		foreach ($list as $entry) {
			$html .= "
\n";
			$html .= 'getParam('anchorsPrefix') . $this->titleToIdentifier($depth, $entry['value']) . '">'. $entry['value'] . "\n";
			if (isset($entry['sub']))
				$html .= $this->_getRenderedToc($entry['sub'], ($depth + 1));
			$html .= "
\n";
		}
		$html .= "
\n";
		return ($html);
	}
}


<?php

namespace Ekyna\Component\Commerce\Payment\Calculator;

use Ekyna\Component\Commerce\Common\Currency\CurrencyConverterInterface;
use Ekyna\Component\Commerce\Common\Util\Money;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Model\PaymentSubjectInterface;

/**
 * Class PaymentCalculator
 * @package Ekyna\Component\Commerce\Payment\Calculator
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class PaymentCalculator implements PaymentCalculatorInterface
{
    /**
     * @var CurrencyConverterInterface
     */
    private $currencyConverter;

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

    /**
     * @inheritdoc
     */
    public function convertPaymentAmount(PaymentInterface $payment, $currency)
    {
        $paymentCurrency = $payment->getCurrency()->getCode();

        if ($paymentCurrency === $currency) {
            return Money::round($payment->getAmount(), $currency);
        }

        if (
            !is_null($rate = $payment->getExchangeRate()) &&
            $currency === $this->currencyConverter->getDefaultCurrency()
        ) {
            return $this->currencyConverter->convertWithRate($payment->getAmount(), 1 / $rate, $currency);
        }

        return $this->currencyConverter->convert(
            $payment->getAmount(),
            $payment->getCurrency()->getCode(),
            $currency,
            $payment->getCreatedAt()
        );
    }

    /**
     * @inheritdoc
     */
    public function calculatePaidTotal(PaymentSubjectInterface $subject)
    {
        $currency = $subject->getCurrency()->getCode();

        $total = 0;

        foreach ($subject->getPayments() as $payment) {
            if (!$payment->getMethod()->isOutstanding() && PaymentStates::isPaidState($payment->getState())) {
                $total += $this->convertPaymentAmount($payment, $currency);
            }
        }

        return $total;
    }

    /**
     * @inheritdoc
     */
    public function calculateOutstandingAcceptedTotal(PaymentSubjectInterface $subject)
    {
        $currency = $subject->getCurrency()->getCode();

        $total = 0;

        foreach ($subject->getPayments() as $payment) {
            if ($payment->getMethod()->isOutstanding() && PaymentStates::isPaidState($payment->getState())) {
                $total += $this->convertPaymentAmount($payment, $currency);
            }
        }

        return $total;
    }

    /**
     * @inheritdoc
     */
    public function calculateOutstandingExpiredTotal(PaymentSubjectInterface $subject)
    {
        $currency = $subject->getCurrency()->getCode();

        $total = 0;

        foreach ($subject->getPayments() as $payment) {
            if ($payment->getMethod()->isOutstanding() && $payment->getState() === PaymentStates::STATE_EXPIRED) {
                $total += $this->convertPaymentAmount($payment, $currency);
            }
        }

        return $total;
    }

    /**
     * @inheritdoc
     */
    public function calculateRefundedTotal(PaymentSubjectInterface $subject)
    {
        return $this->calculateTotalByState($subject, PaymentStates::STATE_REFUNDED);
    }

    /**
     * @inheritdoc
     */
    public function calculateFailedTotal(PaymentSubjectInterface $subject)
    {
        return $this->calculateTotalByState($subject, PaymentStates::STATE_FAILED);
    }

    /**
     * @inheritdoc
     */
    public function calculateCanceledTotal(PaymentSubjectInterface $subject)
    {
        return $this->calculateTotalByState($subject, PaymentStates::STATE_CANCELED);
    }

    /**
     * @inheritdoc
     */
    public function calculateOfflinePendingTotal(PaymentSubjectInterface $subject)
    {
        $currency = $subject->getCurrency()->getCode();

        $total = 0;

        foreach ($subject->getPayments() as $payment) {
            if ($payment->getState() === PaymentStates::STATE_PENDING && $payment->getMethod()->isManual()) {
                $total += $this->convertPaymentAmount($payment, $currency);
            }
        }

        return $total;
    }

    /**
     * Calculates the payments total by state.
     *
     * @param PaymentSubjectInterface $subject
     * @param string                  $state
     *
     * @return float
     */
    protected function calculateTotalByState(PaymentSubjectInterface $subject, $state)
    {
        PaymentStates::isValidState($state, true);

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

        $total = 0;

        foreach ($subject->getPayments() as $payment) {
            if ($payment->getState() === $state) {
                $total += $this->convertPaymentAmount($payment, $currency);
            }
        }

        return $total;
    }

}


<?php

namespace Ekyna\Component\Commerce\Common\Util;

use Symfony\Component\Intl\Intl;

/**
 * Class Money
 * @package Ekyna\Component\Commerce\Common\Util
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class Money
{
    /**
     * @var \Symfony\Component\Intl\ResourceBundle\CurrencyBundleInterface
     */
    static private $currencyBundle;

    /**
     * @var array
     */
    static private $precisions = [];

    /**
     * @var array
     */
    static private $increments = [];

    /**
     * Rounds the amount to currency's precision.
     *
     * @param float  $amount
     * @param string $currency
     *
     * @return float
     *
     * @see \Symfony\Component\Intl\NumberFormatter\NumberFormatter::roundCurrency()
     */
    static public function round($amount, $currency)
    {
        $precision = static::getPrecision($currency);
        $roundingIncrement = static::getRoundingIncrement($currency);

        $amount = round($amount, $precision, \PHP_ROUND_HALF_EVEN);

        if (0 < $roundingIncrement && 0 < $precision) {
            $roundingFactor = $roundingIncrement / pow(10, $precision);
            $amount = round($amount / $roundingFactor) * $roundingFactor;
        }

        return $amount;
    }

    /**
     * Compares two amounts regarding to the currency precision.
     *
     * @param float  $a
     * @param float  $b
     * @param string $currency
     *
     * @see bccomp()
     *
     * @return int  1 if $a > $b

     *              0 if $a == $b

     *             -1 if $a < $b
     */
    static public function compare($a, $b, $currency)
    {
        return bccomp($a, $b, static::getPrecision($currency));
    }

    /**
     * Returns the currency precision.
     *
     * @param string $currency
     *
     * @return int|null
     */
    static public function getPrecision($currency)
    {
        if (isset(static::$precisions[$currency])) {
            return static::$precisions[$currency];
        }

        return static::$precisions[$currency] = static::getCurrencyBundle()->getFractionDigits($currency);
    }

    /**
     * Returns the currency rounding increment.
     *
     * @param string $currency
     *
     * @return float|int|null
     */
    static public function getRoundingIncrement($currency)
    {
        if (isset(static::$increments[$currency])) {
            return static::$increments[$currency];
        }

        return static::$increments[$currency] = static::getCurrencyBundle()->getRoundingIncrement($currency);
    }

    /**
     * Returns the currency bundle.
     *
     * @return \Symfony\Component\Intl\ResourceBundle\CurrencyBundleInterface
     */
    static private function getCurrencyBundle()
    {
        if (null !== static::$currencyBundle) {
            return static::$currencyBundle;
        }

        return static::$currencyBundle = Intl::getCurrencyBundle();
    }
}


<?php

namespace Ekyna\Component\Commerce\Support\EventListener;

use Ekyna\Component\Commerce\Common\Generator\NumberGeneratorInterface;
use Ekyna\Component\Commerce\Exception\UnexpectedValueException;
use Ekyna\Component\Commerce\Support\Model\TicketInterface;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

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

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

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

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

        if ($this->handleUpdate($ticket)) {
            $this->persistenceHelper->persistAndRecompute($ticket, false);
        }
    }

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

        if ($this->handleUpdate($ticket)) {
            $this->persistenceHelper->persistAndRecompute($ticket, false);
        }
    }

    /**
     * Handles the ticket insertion.
     *
     * @param TicketInterface $ticket
     *
     * @return bool
     */
    protected function handleInsert(TicketInterface $ticket)
    {
        return $this->updateNumber($ticket);
    }

    /**
     * Handles the ticket update.
     *
     * @param TicketInterface $ticket
     *
     * @return bool
     */
    protected function handleUpdate(TicketInterface $ticket)
    {
        return $this->updateNumber($ticket);
    }

    /**
     * Updates the number.
     *
     * @param TicketInterface $ticket
     *
     * @return bool Whether the sale number has been update.
     */
    protected function updateNumber(TicketInterface $ticket)
    {
        if (0 == strlen($ticket->getNumber())) {
            $this->numberGenerator->generate($ticket);

            return true;
        }

        return false;
    }

    /**
     * Returns the ticket from the event.
     *
     * @param ResourceEventInterface $event
     *
     * @return TicketInterface
     */
    protected function getTicketFromEvent(ResourceEventInterface $event)
    {
        $ticket = $event->getResource();

        if (!$ticket instanceof TicketInterface) {
            throw new UnexpectedValueException("Expected instance of " . TicketInterface::class);
        }

        return $ticket;
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Model;

use Ekyna\Component\Commerce\Exception\InvalidArgumentException;

/**
 * Class StockAdjustmentReasons
 * @package Ekyna\Component\Commerce\Stock\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StockAdjustmentReasons
{
    const REASON_FAULTY   = 'faulty';
    const REASON_IMPROPER = 'improper';
    const REASON_DEBIT    = 'debit';
    const REASON_CREDIT   = 'credit';
    const REASON_FOUND    = 'found';

    /**
     * Returns all the reasons.
     *
     * @return array
     */
    static public function getReasons()
    {
        return [
            static::REASON_FAULTY,
            static::REASON_IMPROPER,
            static::REASON_FOUND,
            static::REASON_CREDIT,
            static::REASON_DEBIT,
        ];
    }

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

        if ($throw) {
            throw new InvalidArgumentException("Invalid stock adjustment reason.");
        }

        return false;
    }

    /**
     * Returns all the debit reasons.
     *
     * @return array
     */
    static public function getDebitReasons()
    {
        return [
            static::REASON_FAULTY,
            static::REASON_IMPROPER,
            static::REASON_DEBIT,
        ];
    }

    /**
     * Returns whether the given reason is debit or not.
     *
     * @param string $reason
     *
     * @return bool
     */
    static public function isDebitReason($reason)
    {
        static::isValidReason($reason);

        return in_array($reason, static::getDebitReasons(), true);
    }
}


<?php
namespace AuthActions\Lib;

use Cake\Routing\Router;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;

class AuthActions {

/**
 * Holds the rights config.
 *
 * Format:
 * 		'controller' => array(
 * 			'action1' => array(role1, role2)
 * 		),
 * 		'controller2' => array(
 * 			'*' => array('role3')
 * 		)
 *
 * @var string
 */
	protected $_rightsConfig = array();

/**
 * Holds the public actions config
 *
 * @var array
 */
	protected $_publicActions = [];

/**
 * Options
 *
 * @var array
 */
	protected $_options = [
		'camelizedControllerNames' => false
	];

/**
 * Constructor
 *
 * @param array $rightsConfig The controller-actions/rights configuration
 * @param array $publicActions Public actions
 * @param array $options Additional options
 */
	public function __construct(array $rightsConfig, array $publicActions, array $options = []) {
		$this->_rightsConfig = $rightsConfig;
		$this->_publicActions = $publicActions;
		$this->_options = Hash::merge($this->_options, $options);
	}

/**
 * Checks whether the user has access to certain controller action
 *
 * @param array $user user to check
 * @param string $plugin plugin name or null
 * @param string $controller controller name
 * @param string $action action
 * @return bool
 */
	public function isAuthorized($user, $plugin, $controller, $action) {
		$isAuthorized = false;

		if ($plugin) {
			$plugin = Inflector::camelize($plugin);
		}

		if ($this->isPublicAction($plugin, $controller, $action)) {
			$isAuthorized = true;
		} elseif (isset($user['role']) && !empty($controller) && !empty($action)) {
			if ($this->_options['camelizedControllerNames']) {
				$controller = Inflector::camelize($controller);
			} else {
				$controller = Inflector::underscore($controller);
			}

			$key = $controller;
			if (!empty($plugin)) {
				$key = $plugin . '.' . $key;
			}

			if (isset($this->_rightsConfig[$key]['*']) && $this->_rightsConfig[$key]['*'] == '*') {
				$isAuthorized = true;
			} elseif (isset($this->_rightsConfig[$key]['*'])
			&& in_array($user['role'], $this->_rightsConfig[$key]['*'])) {
				$isAuthorized = true;
			} elseif (isset($this->_rightsConfig[$key][$action])
				&& in_array($user['role'], $this->_rightsConfig[$key][$action])) {

				$isAuthorized = true;
			}
		}
		return $isAuthorized;
	}

/**
 * Checks if the given plugin/controller/action combination is configured to be public
 *
 * @param string $plugin plugin name
 * @param string $controller controller name
 * @param string $action action name
 * @return bool
 */
	public function isPublicAction($plugin, $controller, $action) {
		if ($this->_options['camelizedControllerNames']) {
			$controller = Inflector::camelize($controller);
		} else {
			$controller = Inflector::underscore($controller);
		}
		$key = ($plugin ? $plugin . '.' : '') . $controller;

		$isPublic = false;
		if (isset($this->_publicActions[$key])) {
			if ($this->_publicActions[$key] === '*') {
				$isPublic = true;
			} elseif ($this->_publicActions[$key] === $action) {
				$isPublic = true;
			} elseif (is_array($this->_publicActions[$key]) && in_array($action, $this->_publicActions[$key])) {
				$isPublic = true;
			}
		}
		return $isPublic;
	}

/**
 * Checks whether the user is allowed to access a specific URL
 *
 * @param array $user user to check with
 * @param array|string $url url to check
 * @return void
 */
	public function urlAllowed($user, $url) {
		if (empty($url)) {
			return false;
		}

		if (is_array($url)) {
			// prevent plugin confusion
			$url = Hash::merge([
				'plugin' => null
			], $url);

			$url = Router::url($url);
			// strip off the base path
			$url = Router::normalize($url);
		}
		$route = Router::parse($url);
		if (empty($route['controller']) || empty($route['action'])) {
			return false;
		}
		return $this->isAuthorized($user, $route['plugin'], $route['controller'], $route['action']);
	}
}


<?php

namespace Ekyna\Component\Commerce\Stock\Model;

/**
 * Class StockSubjectModes
 * @package Ekyna\Component\Commerce\Stock\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class StockSubjectModes
{
    const MODE_DISABLED     = 'disabled';
    const MODE_MANUAL       = 'manual';
    const MODE_AUTO         = 'auto';
    const MODE_JUST_IN_TIME = 'just_in_time';

    /**
     * Returns all the modes.
     *
     * @return array
     */
    static public function getModes()
    {
        return [
            static::MODE_DISABLED,
            static::MODE_MANUAL,
            static::MODE_AUTO,
            static::MODE_JUST_IN_TIME,
        ];
    }

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

    /**
     * Returns whether the mode A is better than the mode B.
     *
     * @param string $modeA
     * @param string $modeB
     *
     * @return bool
     */
    static public function isBetterMode($modeA, $modeB)
    {
        // TODO Find something more explicit than 'better' (availability ?)

        // TODO assert valid states ?

        if ($modeA === static::MODE_DISABLED) {
            return $modeB !== static::MODE_DISABLED;
        } elseif ($modeA === static::MODE_JUST_IN_TIME) {
            return in_array($modeB, [static::MODE_MANUAL, static::MODE_AUTO], true);
        }

        return false;
    }

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


<?php

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

use Ekyna\Component\Commerce\Common\Model\AddressInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class AddressValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Étienne Dauvergne <contact@ekyna.com>
 */
class AddressValidator extends ConstraintValidator
{
    /**
     * @var \Symfony\Component\PropertyAccess\PropertyAccessor
     */
    private $propertyAccessor;

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

        if (!$address instanceof AddressInterface) {
            throw new UnexpectedTypeException($address, AddressInterface::class);
        }
        if (!$constraint instanceof Address) {
            throw new UnexpectedTypeException($constraint, Address::class);
        }

        $config = [
            'company'    => [
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'street'     => [
                new Assert\NotBlank(),
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'complement' => [
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'supplement' => [
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'extra' => [
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'postalCode' => [
                new Assert\NotBlank(),
                new Assert\Length([
                    'min' => 2,
                    'max' => 10,
                ]),
            ],
            'city'       => [
                new Assert\NotBlank(),
                new Assert\Length([
                    'min' => 2,
                    'max' => 35,
                ]),
            ],
            'country'    => [
                new Assert\NotNull(),
            ],
            'digicode1' => [
                new Assert\Length([
                    'max' => 8,
                ]),
            ],
            'digicode2' => [
                new Assert\Length([
                    'max' => 8,
                ]),
            ],
            'intercom' => [
                new Assert\Length([
                    'max' => 10,
                ]),
            ],
        ];

        if (null !== $country = $address->getCountry()) {
            // TODO Use MisdPhoneNumberBundle's constraint when multiple types handled
            $config['phone'] = [
                new PhoneNumber([
                    'type'          => ['fixed_line', 'voip'],
                    'defaultRegion' => $country->getCode(),
                ]),
            ];
            $config['mobile'] = [
                new PhoneNumber([
                    'type'          => 'mobile',
                    'defaultRegion' => $country->getCode(),
                ]),
            ];

            $zipCodeClass = 'ZipCodeValidator\Constraints\ZipCode';
            if (class_exists($zipCodeClass)) {
                $config['postalCode'][] = new $zipCodeClass([
                    'message'     => $constraint->invalid_zip_code,
                    'iso'         => $country->getCode(),
                    'ignoreEmpty' => true,
                    'strict'      => false,
                ]);
            }
        }

        if ($constraint->company) {
            $config['company'][] = new Assert\NotBlank();
        }
        if ($constraint->phone) {
            $config['phone'][] = new Assert\NotBlank();
        }
        if ($constraint->mobile) {
            $config['mobile'][] = new Assert\NotBlank();
        }

        if (null === $this->propertyAccessor) {
            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
        }

        foreach ($config as $field => $constraints) {
            $violationList = $this
                ->context
                ->getValidator()
                ->validate($this->propertyAccessor->getValue($address, $field), $constraints);

            /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
            foreach ($violationList as $violation) {
                $this->context
                    ->buildViolation($violation->getMessage())
                    ->atPath($field)
                    ->addViolation();
            }
        }

        IdentityValidator::validateIdentity($this->context, $address, [
            'required' => $constraint->identity,
        ]);
    }
}


<?php

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

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

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

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

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

            return;
        }

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

            return;
        }

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

            return;
        }

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

            return;
        }

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

            return;
        }

        $request->markUnknown();
    }

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


<?php

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

use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Repository\SaleRepositoryInterface;
use Ekyna\Component\Commerce\Customer\Model\CustomerInterface;
use Ekyna\Component\Commerce\Subject\Model\SubjectInterface;
use Ekyna\Component\Resource\Doctrine\ORM\ResourceRepository;

/**
 * Class AbstractSaleRepository
 * @package Ekyna\Component\Commerce\Common\Repository
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractSaleRepository extends ResourceRepository implements SaleRepositoryInterface
{
    /**
     * @inheritdoc
     */
    public function findOneById($id)
    {
        $qb = $this->getOneQueryBuilder('o');

        $sale = $qb
            ->andWhere($qb->expr()->eq('o.id', ':id'))
            ->getQuery()
            ->useQueryCache(true)
            ->setParameter('id', $id)
            ->getOneOrNullResult();

        if (null !== $sale) {
            $this
                ->loadLines($sale)
                ->loadPayments($sale);
        }

        return $sale;
    }

    /**
     * @inheritdoc
     */
    public function findOneByKey($key)
    {
        $qb = $this->getOneQueryBuilder('o');

        $sale = $qb
            ->andWhere($qb->expr()->eq('o.key', ':key'))
            ->getQuery()
            ->useQueryCache(true)
            ->setParameter('key', $key)
            ->getOneOrNullResult();

        if (null !== $sale) {
            $this
                ->loadLines($sale)
                ->loadPayments($sale);
        }

        return $sale;
    }

    /**
     * @inheritdoc
     */
    public function findOneByNumber($number)
    {
        $qb = $this->getOneQueryBuilder('o');

        $sale = $qb
            ->andWhere($qb->expr()->eq('o.number', ':number'))
            ->getQuery()
            ->useQueryCache(true)
            ->setParameter('number', $number)
            ->getOneOrNullResult();

        return $sale;
    }

    /**
     * @inheritdoc
     */
    public function findByCustomer(CustomerInterface $customer, array $states = [], $withChildren = false)
    {
        $qb = $this->createQueryBuilder('o');

        if ($withChildren && $customer->hasChildren()) {
            $qb->andWhere($qb->expr()->in('o.customer', ':customers'));
            $parameters = ['customers' => array_merge([$customer], $customer->getChildren()->toArray())];
        } else {
            $qb->andWhere($qb->expr()->eq('o.customer', ':customer'));
            $parameters = ['customer' => $customer];
        }

        $qb->addOrderBy('o.createdAt', 'DESC');

        if (!empty($states)) {
            $qb->andWhere($qb->expr()->in('o.state', ':states'));

            $parameters['states'] = $states;
        }

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

    /**
     * @inheritdoc
     */
    public function findOneByCustomerAndNumber(CustomerInterface $customer, $number)
    {
        $qb = $this->createQueryBuilder('o');

        $sale = $qb
            ->join('o.customer', 'c')
            ->andWhere($qb->expr()->orX(
                $qb->expr()->eq('c', ':customer'),
                $qb->expr()->eq('c.parent', ':customer')
            ))
            ->andWhere($qb->expr()->eq('o.number', ':number'))
            ->getQuery()
            ->setParameters([
                'customer' => $customer,
                'number'   => $number,
            ])
            ->getOneOrNullResult();

        if (null !== $sale) {
            $this
                ->loadLines($sale)
                ->loadPayments($sale);
        }

        return $sale;
    }

    /**
     * @inheritdoc
     */
    public function findBySubject(SubjectInterface  $subject, array $states = [])
    {
        $qb = $this->createQueryBuilder('o');
        $qb
            ->join('o.items', 'i')
            ->leftJoin('i.children', 'c')
            ->leftJoin('c.children', 'sc')
            ->andWhere($qb->expr()->orX(
                $qb->expr()->andX(
                    $qb->expr()->eq('i.subjectIdentity.provider', ':provider'),
                    $qb->expr()->eq('i.subjectIdentity.identifier', ':identifier')
                ),
                $qb->expr()->andX(
                    $qb->expr()->eq('c.subjectIdentity.provider', ':provider'),
                    $qb->expr()->eq('c.subjectIdentity.identifier', ':identifier')
                ),
                $qb->expr()->andX(
                    $qb->expr()->eq('sc.subjectIdentity.provider', ':provider'),
                    $qb->expr()->eq('sc.subjectIdentity.identifier', ':identifier')
                )
            ));

        $parameters = [
            'provider'   => $subject::getProviderName(),
            'identifier' => $subject->getId(),
        ];

        if (!empty($states)) {
            $qb->andWhere($qb->expr()->in('o.state', ':states'));

            $parameters['states'] = $states;
        }

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

    /**
     * Returns the "find one result" query builder.
     *
     * @param string $alias
     * @param string $indexBy
     *
     * @return \Doctrine\ORM\QueryBuilder
     */
    protected function getOneQueryBuilder($alias = null, $indexBy = null)
    {
        return $this
            ->createQueryBuilder($alias, $indexBy)
            ->select(
                $alias,
                'customer',
                'customer_group',
                'invoice_address',
                'delivery_address',
                'shipment_method',
                'currency'
            )
            ->leftJoin($alias . '.customer', 'customer')
            ->leftJoin($alias . '.customerGroup', 'customer_group')
            ->leftJoin($alias . '.invoiceAddress', 'invoice_address')
            ->leftJoin($alias . '.deliveryAddress', 'delivery_address')
            ->leftJoin($alias . '.shipmentMethod', 'shipment_method')
            ->leftJoin($alias . '.currency', 'currency')
            ->setMaxResults(1);
    }

    /**
     * Loads the sale lines (items / adjustments).
     *
     * @param SaleInterface $sale
     *
     * @return $this|AbstractSaleRepository
     */
    protected function loadLines(SaleInterface $sale)
    {
        if (null !== $sale) {
            $qb = $this->createQueryBuilder('o');
            $qb
                ->select('PARTIAL o.{id}', 'item', 'adjustment', 'sub_item', 'sub_adjustment')
                ->leftJoin('o.items', 'item')
                ->leftJoin('item.adjustments', 'adjustment')
                ->leftJoin('item.children', 'sub_item')
                ->leftJoin('sub_item.adjustments', 'sub_adjustment')
                ->andWhere($qb->expr()->eq('o.id', ':id'))
                ->getQuery()
                ->useQueryCache(true)
                ->setParameter('id', $sale->getId())
                ->getResult();
        }

        return $this;
    }

    /**
     * Loads the sale payments.
     *
     * @param SaleInterface $sale
     *
     * @return $this|AbstractSaleRepository
     */
    protected function loadPayments(SaleInterface $sale)
    {
        if (null !== $sale) {
            $qb = $this->createQueryBuilder('o');
            $qb
                ->select('PARTIAL o.{id}', 'payment')
                ->leftJoin('o.payments', 'payment')
                ->andWhere($qb->expr()->eq('o.id', ':id'))
                ->getQuery()
                ->useQueryCache(true)
                ->setParameter('id', $sale->getId())
                ->getResult();
        }

        return $this;
    }
}


<?php

namespace Ekyna\Component\Commerce\Payment\Watcher;

use Ekyna\Component\Commerce\Bridge\Payum\OutstandingBalance\Constants;
use Ekyna\Component\Commerce\Payment\Model;
use Ekyna\Component\Commerce\Payment\Repository;

/**
 * Class OutstandingWatcher
 * @package Ekyna\Component\Commerce\Payment\Watcher
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class OutstandingWatcher implements WatcherInterface
{
    /**
     * @var Repository\PaymentTermRepositoryInterface
     */
    private $termRepository;

    /**
     * @var Repository\PaymentMethodRepositoryInterface
     */
    private $methodRepository;

    /**
     * Constructor.
     *
     * @param Repository\PaymentTermRepositoryInterface $termRepository
     * @param Repository\PaymentMethodRepositoryInterface $methodRepository
     */
    public function __construct(
        Repository\PaymentTermRepositoryInterface $termRepository,
        Repository\PaymentMethodRepositoryInterface $methodRepository
    ) {
        $this->termRepository = $termRepository;
        $this->methodRepository = $methodRepository;
    }

    /**
     * Watch for outstanding payments.
     *
     * @param Repository\PaymentRepositoryInterface $paymentRepository
     *
     * @return bool Whether some payments have been updated.
     */
    public function watch(Repository\PaymentRepositoryInterface $paymentRepository)
    {
        if (null === $term = $this->termRepository->findLongest()) {
            return false;
        }

        $today = new \DateTime();
        $today->setTime(0, 0, 0);

        $fromDate = clone $today;
        $fromDate->modify('-1 year');

        $states = [Model\PaymentStates::STATE_AUTHORIZED, Model\PaymentStates::STATE_CAPTURED];

        /** @var Model\PaymentMethodInterface $method */
        $method = $this->methodRepository->findOneBy([
            'factoryName' => Constants::FACTORY_NAME,
        ]);

        if (!$method || !$method->isOutstanding()) {
            return false;
        }

        $result = false;
        $payments = $paymentRepository->findByMethodAndStates($method, $states, $fromDate);

        foreach ($payments as $payment) {
            $sale = $payment->getSale();

            // Sale may not have a outstanding limit date
            if (null === $date = $sale->getOutstandingDate()) {
                continue;
            }

            // If outstanding limit date is past
            $diff = $date->diff($today);
            if (0 < $diff->days && !$diff->invert) {
                $payment->setState(Model\PaymentStates::STATE_EXPIRED);

                $this->persist($payment);

                $result = true;
            }
        }

        return $result;
    }

    /**
     * Persists the payment.
     *
     * @param Model\PaymentInterface $payment
     */
    abstract protected function persist(Model\PaymentInterface $payment);
}


<?php

namespace Ekyna\Component\Commerce\Shipment\Model;

use Doctrine\Common\Collections\ArrayCollection;

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

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

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

    /**
     * @var ArrayCollection|ShipmentLabelInterface[]
     */
    protected $labels;

    /**
     * Initializes the shipment data.
     */
    protected function initializeShipmentData()
    {
        $this->labels = new ArrayCollection();
    }

    /**
     * Returns the weight.
     *
     * @return float
     */
    public function getWeight()
    {
        return $this->weight;
    }

    /**
     * Sets the weight.
     *
     * @param float $weight
     *
     * @return $this|ShipmentDataInterface
     */
    public function setWeight($weight)
    {
        $this->weight = (float)$weight;

        return $this;
    }

    /**
     * Returns the valorization.
     *
     * @return float
     */
    public function getValorization()
    {
        return $this->valorization;
    }

    /**
     * Sets the valorization.
     *
     * @param float $valorization
     */
    public function setValorization($valorization)
    {
        $this->valorization = (float)$valorization;
    }

    /**
     * Returns the tracking number.
     *
     * @return string
     */
    public function getTrackingNumber()
    {
        return $this->trackingNumber;
    }

    /**
     * Sets the tracking number.
     *
     * @param string $number
     *
     * @return $this|ShipmentDataInterface
     */
    public function setTrackingNumber($number)
    {
        $this->trackingNumber = $number;

        return $this;
    }

    /**
     * Returns whether the shipment/parcel has labels.
     *
     * @return bool
     */
    public function hasLabels()
    {
        return 0 < $this->labels->count();
    }

    /**
     * Returns the labels.
     *
     * @return ArrayCollection|ShipmentLabelInterface[]
     */
    public function getLabels()
    {
        return $this->labels;
    }

    /**
     * Returns whether the shipment/parcel has the given label.
     *
     * @param ShipmentLabelInterface $label
     *
     * @return bool
     */
    public function hasLabel(ShipmentLabelInterface $label)
    {
        return $this->labels->contains($label);
    }

    /**
     * Adds the label.
     *
     * @param ShipmentLabelInterface $label
     *
     * @return $this|ShipmentDataInterface
     */
    public function addLabel(ShipmentLabelInterface $label)
    {
        if (!$this->hasLabel($label)) {
            $this->labels->add($label);

            if ($this instanceof ShipmentInterface) {
                $label->setShipment($this)->setParcel(null);
            } else {
                $label->setParcel($this)->setShipment(null);
            }
        }

        return $this;
    }

    /**
     * Removes the label.
     *
     * @param ShipmentLabelInterface $label
     *
     * @return $this|ShipmentDataInterface
     */
    public function removeLabel(ShipmentLabelInterface $label)
    {
        if ($this->hasLabel($label)) {
            $this->labels->removeElement($label);

            if ($this instanceof ShipmentInterface) {
                $label->setShipment(null);
            } else {
                $label->setParcel(null);
            }
        }

        return $this;
    }
}


<?php

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

use Ekyna\Component\Commerce\Subject\Model\SubjectInterface;
use Ekyna\Component\Commerce\Supplier\Model\SupplierInterface;
use Ekyna\Component\Commerce\Supplier\Model\SupplierProductInterface;
use Ekyna\Component\Commerce\Supplier\Repository\SupplierProductRepositoryInterface;
use Ekyna\Component\Resource\Doctrine\ORM\ResourceRepository;

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

    /**
     * @var \Doctrine\ORM\Query
     */
    private $getAvailableSumBySubjectQuery;

    /**
     * @var \Doctrine\ORM\Query
     */
    private $getMinEdaBySubjectQuery;

    /**
     * @var \Doctrine\ORM\Query
     */
    private $findBySubjectAndSupplierQuery;

    /**
     * @inheritDoc
     */
    public function findBySupplier(SupplierInterface $supplier)
    {
        return $this->findBy(['supplier' => $supplier]);
    }

    /**
     * @inheritDoc
     */
    public function findBySubject(SubjectInterface $subject)
    {
        return $this
            ->getFindBySubjectQuery()
            ->setParameters([
                'provider'   => $subject->getProviderName(),
                'identifier' => $subject->getId(),
            ])
            ->getResult();
    }

    /**
     * @inheritDoc
     */
    public function getMinEstimatedDateOfArrivalBySubject(SubjectInterface $subject)
    {
        $result = $this
            ->getGetMinEdaBySubjectQuery()
            ->setParameters([
                'provider'   => $subject->getProviderName(),
                'identifier' => $subject->getId(),
            ])
            ->getSingleScalarResult();

        return null !== $result ? new \DateTime($result) : null;
    }

    /**
     * @inheritDoc
     */
    public function getAvailableQuantitySumBySubject(SubjectInterface $subject)
    {
        return (float)$this
            ->getGetAvailableSumBySubjectQuery()
            ->setParameters([
                'provider'   => $subject->getProviderName(),
                'identifier' => $subject->getId(),
            ])
            ->getSingleScalarResult();
    }

    /**
     * @inheritDoc
     */
    public function findBySubjectAndSupplier(
        SubjectInterface $subject,
        SupplierInterface $supplier,
        SupplierProductInterface $exclude = null
    ) {
        $parameters = [
            'provider'   => $subject->getProviderName(),
            'identifier' => $subject->getId(),
            'supplier'   => $supplier,
        ];

        if (null === $exclude) {
            return $this
                ->getFindBySubjectAndSupplierQuery()
                ->setParameters($parameters)
                ->getOneOrNullResult();
        }

        $as = $this->getAlias();
        $qb = $this->createFindBySubjectQueryBuilder();

        $parameters['exclude'] = $exclude->getId();

        return $qb
            ->andWhere($qb->expr()->eq($as . '.supplier', ':supplier'))
            ->andWhere($qb->expr()->neq($as . '.id', ':exclude'))
            ->getQuery()
            ->setParameters($parameters)
            ->getOneOrNullResult();
    }

    /**
     * Returns the "find by subject" query.
     *
     * @return \Doctrine\ORM\Query
     */
    protected function getFindBySubjectQuery()
    {
        if (null !== $this->findBySubjectQuery) {
            return $this->findBySubjectQuery;
        }

        $qb = $this->createFindBySubjectQueryBuilder();

        return $this->findBySubjectQuery = $qb->getQuery();
    }

    /**
     * Returns the "get available quantity sum by subject" query.
     *
     * @return \Doctrine\ORM\Query
     */
    protected function getGetAvailableSumBySubjectQuery()
    {
        if (null !== $this->getAvailableSumBySubjectQuery) {
            return $this->getAvailableSumBySubjectQuery;
        }

        $as = $this->getAlias();
        $qb = $this->createFindBySubjectQueryBuilder();

        $qb
            ->andWhere($qb->expr()->gte($as . '.availableStock', 0))
            ->select('SUM(' . $as . '.availableStock) as available');

        return $this->getAvailableSumBySubjectQuery = $qb->getQuery();
    }

    /**
     * Returns the "get estimated date of arrival by subject" query.
     *
     * @return \Doctrine\ORM\Query
     */
    protected function getGetMinEdaBySubjectQuery()
    {
        if (null !== $this->getMinEdaBySubjectQuery) {
            return $this->getMinEdaBySubjectQuery;
        }

        $as = $this->getAlias();
        $qb = $this->createFindBySubjectQueryBuilder();

        $qb
            ->andWhere($qb->expr()->isNotNull($as . '.estimatedDateOfArrival'))
            ->andWhere($qb->expr()->orX(
                $qb->expr()->gte($as . '.orderedStock', 0),
                $qb->expr()->gte($as . '.availableStock', 0)
            ))
            ->select('MIN(' . $as . '.estimatedDateOfArrival) as eda');

        return $this->getMinEdaBySubjectQuery = $qb->getQuery();
    }

    /**
     * Returns the "find by subject and supplier" query.
     *
     * @return \Doctrine\ORM\Query
     */
    protected function getFindBySubjectAndSupplierQuery()
    {
        if (null !== $this->findBySubjectAndSupplierQuery) {
            return $this->findBySubjectAndSupplierQuery;
        }

        $qb = $this->createFindBySubjectQueryBuilder();

        return $this->findBySubjectAndSupplierQuery = $qb
            ->andWhere($qb->expr()->eq($this->getAlias() . '.supplier', ':supplier'))
            ->getQuery();
    }

    /**
     * Creates a "find by subject" query builder.
     *
     * @return \Doctrine\ORM\QueryBuilder
     */
    private function createFindBySubjectQueryBuilder()
    {
        $as = $this->getAlias();
        $qb = $this->createQueryBuilder();

        return $qb
            ->andWhere($qb->expr()->eq($as . '.subjectIdentity.provider', ':provider'))
            ->andWhere($qb->expr()->eq($as . '.subjectIdentity.identifier', ':identifier'));
    }

    /**
     * @inheritDoc
     */
    protected function getAlias()
    {
        return 'sp';
    }
}