<?php

namespace Ekyna\Component\Commerce\Stock\Assigner;

use Ekyna\Component\Commerce\Common\Factory\SaleFactoryInterface;
use Ekyna\Component\Commerce\Common\Model\SaleItemInterface;
use Ekyna\Component\Commerce\Exception\StockLogicException;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceLineInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceTypes;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentItemInterface;
use Ekyna\Component\Commerce\Stock\Model\StockAssignmentInterface;
use Ekyna\Component\Commerce\Stock\Model\StockAssignmentsInterface;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectInterface;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectModes;
use Ekyna\Component\Commerce\Stock\Model\StockUnitInterface;
use Ekyna\Component\Commerce\Stock\Resolver\StockUnitResolverInterface;
use Ekyna\Component\Commerce\Stock\Updater\StockAssignmentUpdaterInterface;
use Ekyna\Component\Commerce\Stock\Updater\StockUnitUpdaterInterface;
use Ekyna\Component\Commerce\Subject\SubjectHelperInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

/**
 * Class StockUnitAssigner
 * @package Ekyna\Component\Commerce\Stock\Assigner
 * @author  Etienne Dauvergne <contact@ekyna.com>
 *
 * This class is responsible for assigning sale items to stock units.
 */
class StockUnitAssigner implements StockUnitAssignerInterface
{
    /**
     * @var PersistenceHelperInterface
     */
    protected $persistenceHelper;

    /**
     * @var SubjectHelperInterface
     */
    protected $subjectHelper;

    /**
     * @var StockUnitResolverInterface
     */
    protected $unitResolver;

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

    /**
     * @var StockAssignmentUpdaterInterface
     */
    protected $assignmentUpdater;

    /**
     * @var SaleFactoryInterface
     */
    protected $saleFactory;

    /**
     * Constructor.
     *
     * @param PersistenceHelperInterface      $persistenceHelper
     * @param SubjectHelperInterface          $subjectHelper
     * @param StockUnitResolverInterface      $unitResolver
     * @param StockUnitUpdaterInterface       $unitUpdater
     * @param StockAssignmentUpdaterInterface $assignmentUpdater
     * @param SaleFactoryInterface            $saleFactory
     */
    public function __construct(
        PersistenceHelperInterface $persistenceHelper,
        SubjectHelperInterface $subjectHelper,
        StockUnitResolverInterface $unitResolver,
        StockUnitUpdaterInterface $unitUpdater,
        StockAssignmentUpdaterInterface $assignmentUpdater,
        SaleFactoryInterface $saleFactory
    ) {
        $this->persistenceHelper = $persistenceHelper;
        $this->subjectHelper = $subjectHelper;
        $this->unitResolver = $unitResolver;
        $this->unitUpdater = $unitUpdater;
        $this->assignmentUpdater = $assignmentUpdater;
        $this->saleFactory = $saleFactory;
    }

    /**
     * @inheritdoc
     */
    public function assignSaleItem(SaleItemInterface $item)
    {
        // Abort if not supported
        if (!$this->supportsAssignment($item)) {
            return;
        }

        // Don't assign twice
        if (!empty($this->getAssignments($item))) {
            throw new StockLogicException(sprintf(
                'Item "%s" is already assigned.',
                $item->getDesignation()
            ));
        }

        // Create assignments
        $this->createAssignmentsForQuantity($item, $item->getTotalQuantity());
    }

    /**
     * @inheritdoc
     */
    public function applySaleItem(SaleItemInterface $item)
    {
        // Abort if not supported
        if (null === $assignments = $this->getAssignments($item)) {
            return;
        }

        if (0 == $quantity = $this->resolveSoldDeltaQuantity($item)) {
            return;
        }

        /** @var StockAssignmentsInterface $item */

        // Determine on which stock units the sold quantity change should be dispatched
        $assignments = $this->sortAssignments($assignments);

        // Debit case : reverse the sorted assignments
        if (0 > $quantity) {
            $assignments = array_reverse($assignments);
        }

        /** @var StockAssignmentInterface $assignment */
        foreach ($assignments as $assignment) {
            $quantity -= $this->assignmentUpdater->updateSold($assignment, $quantity, true);
            if (0 == $quantity) {
                return;
            }
        }

        // Remaining debit
        if (0 > $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to dispatch sale item "%s" changed quantity debit over assigned stock units.',
                $item->getDesignation()
            ));
        }

        // Remaining credit
        if (0 < $quantity) {
            $this->createAssignmentsForQuantity($item, $quantity);
        }
    }

    /**
     * @inheritdoc
     */
    public function detachSaleItem(SaleItemInterface $item)
    {
        // Abort if not supported
        if (null === $assignments = $this->getAssignments($item)) {
            return;
        }

        /** @var StockAssignmentsInterface $item */

        // Remove stock assignments and schedule events
        foreach ($assignments as $assignment) {
            $this->removeAssignment($assignment);
        }
    }

    /**
     * @inheritdoc
     */
    public function assignShipmentItem(ShipmentItemInterface $item)
    {
        // Abort if not supported
        if (null === $assignments = $this->getAssignments($item)) {
            return;
        }

        // TODO sort assignments ?
        // TODO Use packaging format

        $quantity = $item->getQuantity();
        $return = $item->getShipment()->isReturn();

        foreach ($assignments as $assignment) {
            if ($return) {
                $quantity += $this->assignmentUpdater->updateShipped($assignment, -$quantity, true);
            } else {
                $quantity -= $this->assignmentUpdater->updateShipped($assignment, $quantity, true);
            }
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to assign shipment item "%s".',
                $item->getSaleItem()->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function applyShipmentItem(ShipmentItemInterface $item)
    {
        // Abort if not supported
        if (null === $assignments = $this->getAssignments($item)) {
            return;
        }

        // Resolve quantity change
        if (!$this->persistenceHelper->isChanged($item, 'quantity')) {
            return;
        }
        list($old, $new) = $this->persistenceHelper->getChangeSet($item, 'quantity');
        if (0 == $quantity = $new - $old) {
            return;
        }

        // TODO sort assignments ? (reverse for debit)
        // TODO Use packaging format

        $return = $item->getShipment()->isReturn();

        // Update assignments
        foreach ($assignments as $assignment) {
            if ($return) {
                $quantity += $this->assignmentUpdater->updateShipped($assignment, -$quantity, true);
            } else {
                $quantity -= $this->assignmentUpdater->updateShipped($assignment, $quantity, true);
            }

            if (0 == $quantity) {
                return;
            }
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to apply shipment item "%s".',
                $item->getSaleItem()->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function detachShipmentItem(ShipmentItemInterface $item)
    {
        // Abort if not supported
        if (null === $assignments = $this->getAssignments($item)) {
            return;
        }

        // TODO sort assignments ? (reverse for debit)
        // TODO Use packaging format

        // Get previous quantity if it has changed
        if ($this->persistenceHelper->isChanged($item, 'quantity')) {
            $quantity = $this->persistenceHelper->getChangeSet($item, 'quantity')[0];
        } else {
            $quantity = $item->getQuantity();
        }

        // Get shipment from change set if needed
        if (null === $shipment = $item->getShipment()) {
            $shipment = $this->persistenceHelper->getChangeSet($item, 'shipment')[0];
        }

        $return = $shipment->isReturn();

        // Update assignments
        foreach ($assignments as $assignment) {
            if ($return) {
                $quantity -= $this->assignmentUpdater->updateShipped($assignment, $quantity, true);
            } else {
                $quantity += $this->assignmentUpdater->updateShipped($assignment, -$quantity, true);
            }
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to detach shipment item "%s".',
                $item->getSaleItem()->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function assignInvoiceLine(InvoiceLineInterface $line)
    {
        // Abort if not credit
        if (!InvoiceTypes::isCredit($line->getInvoice()->getType())) {
            return;
        }

        // Abort if not supported
        if (null === $assignments = $this->getAssignments($line)) {
            return;
        }

        // TODO sort assignments ?
        // TODO Use packaging format

        $quantity = $line->getQuantity();

        foreach ($assignments as $assignment) {
            $quantity += $this->assignmentUpdater->updateSold($assignment, -$quantity, true);
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to assign invoice line "%s".',
                $line->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function applyInvoiceLine(InvoiceLineInterface $line)
    {
        // Abort if not credit
        if (!InvoiceTypes::isCredit($line->getInvoice()->getType())) {
            return;
        }

        // Abort if not supported
        if (null === $assignments = $this->getAssignments($line)) {
            return;
        }

        // Resolve quantity change
        if (!$this->persistenceHelper->isChanged($line, 'quantity')) {
            return;
        }
        list($old, $new) = $this->persistenceHelper->getChangeSet($line, 'quantity');
        if (0 == $quantity = $new - $old) {
            return;
        }

        // TODO sort assignments ? (reverse for debit)
        // TODO Use packaging format

        // Update assignments
        foreach ($assignments as $assignment) {
            $quantity += $this->assignmentUpdater->updateSold($assignment, -$quantity, true);

            if (0 == $quantity) {
                return;
            }
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to apply invoice line "%s".',
                $line->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function detachInvoiceLine(InvoiceLineInterface $line)
    {
        // Abort if not credit
        // TODO This might be a problem :
        // we should never call this method when invoice is null
        // cause we won't be able to test if invoice is credit
        // TODO get invoice from change set
        if (null === $invoice = $line->getInvoice()) {
            $invoice = $this->persistenceHelper->getChangeSet($line, 'invoice')[0];
        }

        if (!InvoiceTypes::isCredit($invoice->getType())) {
            return;
        }

        // Abort if not supported
        if (null === $assignments = $this->getAssignments($line)) {
            return;
        }

        // TODO sort assignments ? (reverse for debit)
        // TODO Use packaging format

        $quantity = $line->getQuantity();

        // Update assignments
        foreach ($assignments as $assignment) {
            $quantity -= $this->assignmentUpdater->updateSold($assignment, $quantity, true);
        }

        // Remaining quantity
        if (0 != $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to detach invoice line "%s".',
                $line->getDesignation()
            ));
        }
    }

    /**
     * @inheritdoc
     */
    public function supportsAssignment(SaleItemInterface $item)
    {
        if (!$item instanceof StockAssignmentsInterface) {
            return false;
        }

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

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

        if ($subject->isStockCompound()) {
            return false;
        }

        if ($subject->getStockMode() === StockSubjectModes::MODE_DISABLED) {
            return false;
        }

        return true;
    }

    /**
     * Returns the item's stock assignments, or null if not supported.
     *
     * @param mixed $item
     *
     * @return null|StockAssignmentInterface[]
     */
    protected function getAssignments($item)
    {
        if ($item instanceof ShipmentItemInterface) {
            $item = $item->getSaleItem();
        } elseif ($item instanceof InvoiceLineInterface) {
            if (!$item = $item->getSaleItem()) {
                return null;
            }
        }

        if (!$this->supportsAssignment($item)) {
            return null;
        }

        return $item->getStockAssignments()->toArray();
    }

    /**
     * Removes a single assignment.
     *
     * @param StockAssignmentInterface $assignment
     */
    protected function removeAssignment(StockAssignmentInterface $assignment)
    {
        $this->unitUpdater->updateSold($assignment->getStockUnit(), -$assignment->getSoldQuantity(), true);

        $assignment
            ->setSaleItem(null)
            ->setStockUnit(null);

        $this->persistenceHelper->remove($assignment);
    }

    /**
     * Creates the sale item assignments for the given quantity.
     *
     * @param SaleItemInterface $item
     * @param float             $quantity
     *
     * @throws StockLogicException If assignment creation fails.
     */
    protected function createAssignmentsForQuantity(SaleItemInterface $item, $quantity)
    {
        if (0 >= $quantity) {
            return;
        }

        // Find enough available stock units
        $stockUnits = $this->sortStockUnits($this->unitResolver->findAssignable($item));

        foreach ($stockUnits as $stockUnit) {
            $assignment = $this->saleFactory->createStockAssignmentForItem($item);
            $assignment
                ->setSaleItem($item)
                ->setStockUnit($stockUnit);

            $quantity -= $this->assignmentUpdater->updateSold($assignment, $quantity);

            if (0 == $quantity) {
                return;
            }
        }

        // Remaining quantity
        if (0 < $quantity) {
            $stockUnit = $this->unitResolver->createBySubjectRelative($item);

            $assignment = $this->saleFactory->createStockAssignmentForItem($item);
            $assignment
                ->setSaleItem($item)
                ->setStockUnit($stockUnit);

            $quantity -= $this->assignmentUpdater->updateSold($assignment, $quantity);
        }

        if (0 < $quantity) {
            throw new StockLogicException(sprintf(
                'Failed to create assignments for item "%s".',
                $item->getDesignation()
            ));
        }
    }

    /**
     * Resolves the assignments update's delta quantity.
     *
     * @param SaleItemInterface $item
     *
     * @return float
     */
    protected function resolveSoldDeltaQuantity(SaleItemInterface $item)
    {
        $old = $new = $item->getQuantity();

        // Own item quantity changes
        if ($this->persistenceHelper->isChanged($item, 'quantity')) {
            list($old, $new) = $this->persistenceHelper->getChangeSet($item, 'quantity');
        }

        // Parent items quantity changes
        $parent = $item;
        while (null !== $parent = $parent->getParent()) {
            if ($this->persistenceHelper->isChanged($parent, 'quantity')) {
                list($parentOld, $parentNew) = $this->persistenceHelper->getChangeSet($parent, 'quantity');
            } else {
                $parentOld = $parentNew = $parent->getQuantity();
            }
            $old *= $parentOld;
            $new *= $parentNew;
        }

        // Sale released change
        $sale = $item->getSale();
        $shippedOld = $shippedNew = 0;
        $f = $t = false;
        if ($this->persistenceHelper->isChanged($sale, 'released')) {
            list($f, $t) = $this->persistenceHelper->getChangeSet($sale, 'released');
        } elseif ($item->getSale()->isReleased()) {
            $f = $t = true;
        }
        if ($f || $t) {
            /** @var StockAssignmentsInterface $item */
            foreach ($item->getStockAssignments() as $assignment) {
                if ($this->persistenceHelper->isChanged($assignment, 'shippedQuantity')) {
                    list($o, $n) = $this->persistenceHelper->getChangeSet($assignment, 'shippedQuantity');
                } else {
                    $o = $n = $assignment->getShippedQuantity();
                }
                if ($f) {
                    $shippedOld += $o;
                }
                if ($t) {
                    $shippedNew += $n;
                }
            }

            if ($f) {
                $old = min($old, $shippedOld);
            }
            if ($t) {
                $new = min($new, $shippedNew);
            }
        }

        return $new - $old;
    }

    /**
     * Sorts assignments.
     *
     * @param array $assignments
     *
     * @return array
     */
    protected function sortAssignments(array $assignments)
    {
        usort($assignments, function (StockAssignmentInterface $a1, StockAssignmentInterface $a2) {
            $u1 = $a1->getStockUnit();
            $u2 = $a2->getStockUnit();

            return $this->compareStockUnit($u1, $u2);
        });

        return $assignments;
    }

    /**
     * Sorts the stock units.
     *
     * @param array $stockUnits
     *
     * @return array
     */
    protected function sortStockUnits(array $stockUnits)
    {
        usort($stockUnits, [$this, 'compareStockUnit']);

        return $stockUnits;
    }

    /**
     * Sorts the stock units for credit case (sold quantity).
     *
     * @param StockUnitInterface $u1
     * @param StockUnitInterface $u2
     *
     * @return int
     */
    protected function compareStockUnit(StockUnitInterface $u1, StockUnitInterface $u2)
    {
        // TODO Review this code / make it configurable

        // Sorting is made for credit case

        $u1Result = $u1->getReceivedQuantity() + $u1->getAdjustedQuantity();
        $u2Result = $u2->getReceivedQuantity() + $u2->getAdjustedQuantity();

        // Prefer stock units with received/adjusted quantities
        if (0 < $u1Result && 0 == $u2Result) {
            return -1;
        } elseif (0 == $u1Result && 0 < $u2Result) {
            return 1;
        } elseif (0 < $u1Result && 0 < $u2Result) {
            // If both have received quantities, prefer cheapest
            if (0 != $result = $this->compareStockUnitByPrice($u1, $u2)) {
                return $result;
            }
        }

        $u1Result = $u1->getOrderedQuantity() + $u1->getAdjustedQuantity();
        $u2Result = $u2->getOrderedQuantity() + $u2->getAdjustedQuantity();

        // Prefer stock units with ordered quantities
        if (0 < $u1Result && 0 == $u2Result) {
            return -1;
        } elseif (0 == $u1Result && 0 < $u2Result) {
            return 1;
        }

        // By eta DESC
        /*$now = new \DateTime();
        // Positive if future / Negative if past
        $u1Diff = (null !== $date = $u1->getEstimatedDateOfArrival())
                ? $now->diff($date)->format('%R%a')
                : +9999;
        $u2Diff = (null !== $date = $u2->getEstimatedDateOfArrival())
                ? $now->diff($date)->format('%R%a')
                : +9999;
        if (abs($u1Diff) < 30 && abs($u2Diff) < 30) {

        }*/

        // By eta DESC
        if (0 != $result = $this->compareStockUnitByEda($u1, $u2)) {
            return $result;
        }

        // By price ASC
        if (0 != $result = $this->compareStockUnitByPrice($u1, $u2)) {
            return $result;
        }

        return 0;
    }

    /**
     * Compares the units regarding to their price.
     *
     * @param StockUnitInterface $u1
     * @param StockUnitInterface $u2
     *
     * @return int
     */
    protected function compareStockUnitByPrice(StockUnitInterface $u1, StockUnitInterface $u2)
    {
        $u1HasPrice = 0 < $u1->getNetPrice();
        $u2HasPrice = 0 < $u2->getNetPrice();

        if (!$u1HasPrice && $u2HasPrice) {
            return 1;
        }
        if ($u1HasPrice && !$u2HasPrice) {
            return -1;
        }
        if ($u1->getNetPrice() != $u2->getNetPrice()) {
            return $u1->getNetPrice() > $u2->getNetPrice() ? 1 : -1;
        }

        return 0;
    }

    /**
     * Compares the units regarding to their estimated date of arrival.
     *
     * @param StockUnitInterface $u1
     * @param StockUnitInterface $u2
     *
     * @return int
     */
    protected function compareStockUnitByEda(StockUnitInterface $u1, StockUnitInterface $u2)
    {
        $u1HasEda = null !== $u1->getEstimatedDateOfArrival();
        $u2HasEda = null !== $u2->getEstimatedDateOfArrival();

        if (!$u1HasEda && $u2HasEda) {
            return 1;
        }
        if ($u1HasEda && !$u2HasEda) {
            return -1;
        }
        if ($u1->getEstimatedDateOfArrival() != $u2->getEstimatedDateOfArrival()) {
            return $u1->getEstimatedDateOfArrival() > $u2->getEstimatedDateOfArrival() ? 1 : -1;
        }

        return 0;
    }
}


<?php

namespace mii\web;

use mii\core\Component;

class Session extends Component
{
    /**
     * @var  string  cookie name
     */
    protected $name = 'session';
    /**
     * @var  int  cookie lifetime
     */
    protected $lifetime = 0;

    /**
     * @var  array  session data
     */
    protected $_data = [];
    /**
     * @var  bool  session destroyed?
     */
    protected $_destroyed = false;

    protected $_flash = '__flash';

    /**
     *
     * Checks for session cookie without starting session itself
     *
     * @return bool
     */

    public function check_cookie() {
        return isset($_COOKIE[$this->name]);
    }

    /**
     * Get the current session cookie name.
     *
     *     $name = $session->name();
     *
     * @return  string
     */
    public function name() {
        return $this->name;
    }

    /**
     * Get a variable from the session array.
     *
     *     $foo = $session->get('foo');
     *
     * @param   string $key variable name
     * @param   mixed $default default value to return
     * @return  mixed
     */
    public function get($key, $default = null) {
        $this->open();

        return array_key_exists($key, $this->_data) ? $this->_data[$key] : $default;
    }

    public function has($key) {
        $this->open();

        return array_key_exists($key, $this->_data);
    }

    public function open($id = null) {

        if ($this->is_active())
            return;

        if($this->lifetime > 0) {
            ini_set('session.gc_maxlifetime', $this->lifetime);
        }

        // Sync up the session cookie with Cookie parameters
        session_set_cookie_params($this->lifetime,
            \Mii::$app->request->cookie_path,
            \Mii::$app->request->cookie_domain,
            \Mii::$app->request->cookie_secure,
            \Mii::$app->request->cookie_httponly);

        // Do not allow PHP to send Cache-Control headers
        session_cache_limiter(false);

        // Set the session cookie name
        session_name($this->name);

        if ($id) {
            // Set the session id
            session_id($id);
        }

        // Start the session
        @session_start();

        // Use the $_SESSION global for storing data
        $this->_data =& $_SESSION;

        // Write the session at shutdown
        register_shutdown_function([$this, 'close']);

        $this->update_flash_counters();

        return;
    }

    public function is_active() {
        return session_status() === PHP_SESSION_ACTIVE;
    }

    /**
     * Set a variable in the session array.
     *
     *     $session->set('foo', 'bar');
     *
     * @param   string $key variable name
     * @param   mixed $value value
     * @return  $this
     */
    public function set($key, $value) {
        $this->open();

        $this->_data[$key] = $value;

        return $this;
    }

    /**
     * Set a variable by reference.
     *
     *     $session->bind('foo', $foo);
     *
     * @param   string $key variable name
     * @param   mixed $value referenced value
     * @return  $this
     */
    public function bind($key, & $value) {
        $this->open();

        $this->_data[$key] =& $value;

        return $this;
    }

    /**
     * Removes a variable in the session array.
     *
     *     $session->delete('foo');
     *
     * @param   string $key,... variable name
     * @return  $this
     */
    public function delete(...$args) {
        $this->open();

        foreach ($args as $key) {
            unset($this->_data[$key]);
        }

        return $this;
    }

    /**
     * Generates a new session id and returns it.
     *
     *     $id = $session->regenerate();
     *
     * @return  string
     */
    public function regenerate($delete_old = false) {
        if ($this->is_active()) {
            // Regenerate the session id
            @session_regenerate_id($delete_old);
        } else {
            $this->open();
        }

        return session_id();
    }

    /**
     * Sets the last_active timestamp and saves the session.
     *
     *     $session->close();
     *
     * [!!] Any errors that occur during session writing will be logged,
     * but not displayed, because sessions are written after output has
     * been sent.
     *
     */
    public function close(): void {
        if ($this->is_active()) {

            // Set the last active timestamp
            $this->_data['last_active'] = time();

            // Write and close the session
            config('debug') ? session_write_close() : @session_write_close();
        }
    }

    /**
     * Completely destroy the current session.
     *
     */
    public function destroy(): void {
        if ($this->is_active()) {

            session_unset();
            session_destroy();
            $this->_data = [];

            // Make sure the session cannot be restarted
            Cookie::delete($this->name);
        }
    }

    public function id(): string {
        return session_id();
    }

    public function flash($key, $value = true) {
        $this->open();

        $counters = $this->get($this->_flash, []);
        $counters[$key] = 0;
        $this->_data[$key] = $value;
        $this->_data[$this->_flash] = $counters;
    }

    private function update_flash_counters() {
        $counters = $this->get($this->_flash, []);
        if (is_array($counters)) {
            foreach ($counters as $key => $count) {
                if ($count > 0) {
                    unset($counters[$key], $_SESSION[$key]);
                } elseif ($count == 0) {
                    $counters[$key]++;
                }
            }
            $this->_data[$this->_flash] = $counters;
        } else {
            unset($this->_data[$this->_flash]);
        }
    }
}


<?php

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

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

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

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

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

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

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

        // Use parent if available
        if ($customer->hasParent()) {
            $customer = $customer->getParent();
        }

        // If sale has a customer limit
        if (0 < $limit = $sale->getOutstandingLimit()) {
            // Use sale's balance
            $balance = - $sale->getOutstandingAccepted() - $sale->getOutstandingExpired();
        } else {
            // Use customer's limit and balance
            $limit = $customer->getOutstandingLimit();
            $balance = $customer->getOutstandingBalance();
        }

        $details->defaults(array(
            Constants::FIELD_STATUS  => null,
            Constants::FIELD_AMOUNT  => $payment->getAmount(),
            Constants::FIELD_LIMIT   => $limit,
            Constants::FIELD_BALANCE => $balance,
        ));

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

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


<?php

namespace Ekyna\Component\Commerce\Document\Model;

use Ekyna\Component\Commerce\Common\Model as Common;
use Ekyna\Component\Commerce\Common\Util\Money;

/**
 * Class DocumentLine
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class DocumentLine implements DocumentLineInterface
{
    /**
     * @var Document
     */
    protected $document;

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

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

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

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

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

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

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

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

    /**
     * @var array
     */
    protected $discountRates;

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

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

    /**
     * @var array
     */
    protected $taxRates;

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

    /**
     * @var Common\SaleItemInterface
     */
    protected $saleItem;

    /**
     * @var Common\AdjustmentInterface
     */
    protected $saleAdjustment;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->unit = 0;
        $this->quantity = 0;
        $this->discount = 0;
        $this->base = 0;
        $this->taxRates = [];
    }

    /**
     * @inheritdoc
     */
    public function getDocument()
    {
        return $this->document;
    }

    /**
     * @inheritdoc
     */
    public function setDocument(DocumentInterface $document = null)
    {
        if ($this->document !== $document) {
            if ($previous = $this->document) {
                $this->document = null;
                $previous->removeLine($this);
            }

            if ($this->document = $document) {
                $this->document->addLine($this);
            }
        }

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * @inheritdoc
     */
    public function setType($type)
    {
        $this->type = $type;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDesignation()
    {
        return $this->designation;
    }

    /**
     * @inheritdoc
     */
    public function setDesignation($designation)
    {
        $this->designation = $designation;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @inheritdoc
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getReference()
    {
        return $this->reference;
    }

    /**
     * @inheritdoc
     */
    public function setReference($reference)
    {
        $this->reference = $reference;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getUnit(bool $ati = false)
    {
        return $ati ? $this->ati($this->unit) : $this->unit;
    }

    /**
     * @inheritdoc
     */
    public function setUnit($price)
    {
        $this->unit = (float)$price;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getQuantity()
    {
        return $this->quantity;
    }

    /**
     * @inheritdoc
     */
    public function setQuantity($quantity)
    {
        $this->quantity = (float)$quantity;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getGross(bool $ati = false)
    {
        return $ati ? $this->ati($this->gross) : $this->gross;
    }

    /**
     * @inheritdoc
     */
    public function setGross($gross)
    {
        $this->gross = (float)$gross;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDiscount(bool $ati = false)
    {
        return $ati ? $this->ati($this->discount) : $this->discount;
    }

    /**
     * @inheritdoc
     */
    public function setDiscount($total)
    {
        $this->discount = (float)$total;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDiscountRates()
    {
        return $this->discountRates;
    }

    /**
     * @inheritdoc
     */
    public function setDiscountRates(array $rates)
    {
        $this->discountRates = $rates;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getBase(bool $ati = false)
    {
        return $ati ? $this->ati($this->base) : $this->base;
    }

    /**
     * @inheritdoc
     */
    public function setBase($total)
    {
        $this->base = (float)$total;

        return $this;
    }

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

    /**
     * Sets the tax.
     *
     * @param float $tax
     *
     * @return DocumentLine
     */
    public function setTax($tax)
    {
        $this->tax = (float)$tax;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getTaxRates()
    {
        return $this->taxRates;
    }

    /**
     * @inheritdoc
     */
    public function setTaxRates(array $rates)
    {
        $this->taxRates = $rates;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getTotal()
    {
        return $this->total;
    }

    /**
     * @inheritdoc
     */
    public function setTotal($total)
    {
        $this->total = (float)$total;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getSale()
    {
        return $this->getDocument()->getSale();
    }

    /**
     * @inheritdoc
     */
    public function getSaleItem()
    {
        return $this->saleItem;
    }

    /**
     * @inheritdoc
     */
    public function setSaleItem(Common\SaleItemInterface $item = null)
    {
        $this->saleItem = $item;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getSaleAdjustment()
    {
        return $this->saleAdjustment;
    }

    /**
     * @inheritdoc
     */
    public function setSaleAdjustment(Common\AdjustmentInterface $adjustment = null)
    {
        $this->saleAdjustment = $adjustment;

        return $this;
    }

    /**
     * Adds the taxes to the given amount.
     *
     * @param float $amount
     *
     * @return float
     */
    private function ati(float $amount)
    {
        $result = $amount;

        foreach ($this->taxRates as $rate) {
            $result += $amount * $rate / 100;
        }

        return Money::round($result, $this->getDocument()->getCurrency());
    }
}


<?php

namespace mii\web;

use mii\db\Query;
use mii\util\URL;

/**
 * Pagination links generator.
 *
 * @package    Kohana/Pagination
 * @category   Base
 * @author     Kohana Team
 * @copyright  (c) 2008-2009 Kohana Team
 * @license    http://kohanaphp.com/license.html
 */
class Pagination
{

    protected $current_page_source = 'query_string';

    protected $current_page_source_key = 'page';

    // Current page number
    protected $current_page;

    // Total item count
    protected $total_items = 0;

    // How many items to show per page
    protected $items_per_page = 50;

    // Total page count
    protected $total_pages;

    protected $block = 'pagination';

    protected $auto_hide = true;

    protected $first_page_in_url = false;

    // Item offset for the first item displayed on the current page
    protected $current_first_item;

    // Item offset for the last item displayed on the current page
    protected $current_last_item;

    // Previous page number; FALSE if the current page is the first one
    protected $previous_page;

    // Next page number; FALSE if the current page is the last one
    protected $next_page;

    // First page number; FALSE if the current page is the first one
    protected $first_page;

    // Last page number; FALSE if the current page is the last one
    protected $last_page;

    // Query offset
    protected $offset;

    // Route to use for URIs
    protected $route;

    // Parameters to use with Route to create URIs
    protected $route_params = array();

    // Request object
    protected $request;

    /**
     * Creates a new Pagination object.
     *
     * @return  void
     */
    public function __construct(array $config = [], Query &$query = null) {
        foreach ($config as $key => $val)
            $this->$key = $val;

        if (!$this->request)
            $this->request = \Mii::$app->request;

        // Pagination setup
        $this->calculate();

        if($query) {

        }
    }

    /**
     * Loads configuration settings into the object and (re)calculates pagination if needed.
     * Allows you to update config settings after a Pagination object has been constructed.
     *
     * @param   array $config configuration
     * @return  object  Pagination
     */
    public function calculate(array $config = []) {
        foreach ($config as $key => $val)
            $this->$key = $val;

        if ($this->current_page === null) {
            $query_key = $this->current_page_source_key;

            switch ($this->current_page_source) {
                case 'query_string':
                case 'mixed':

                    $this->current_page = (int)$this->request->get($query_key, 1);
                    break;

                case 'route':

                    $this->current_page = (int)$this->request->param($query_key, 1);

                    break;
            }
        }

        // Calculate and clean all pagination variables
        $this->total_items = (int)max(0, $this->total_items);
        $this->items_per_page = (int)max(1, $this->items_per_page);
        $this->total_pages = (int)ceil($this->total_items / $this->items_per_page);
        $this->current_page = (int)min(max(1, $this->current_page), max(1, $this->total_pages));
        $this->current_first_item = (int)min((($this->current_page - 1) * $this->items_per_page) + 1, $this->total_items);
        $this->current_last_item = (int)min($this->current_first_item + $this->items_per_page - 1, $this->total_items);
        $this->previous_page = ($this->current_page > 1) ? $this->current_page - 1 : FALSE;
        $this->next_page = ($this->current_page < $this->total_pages) ? $this->current_page + 1 : FALSE;
        $this->first_page = ($this->current_page === 1) ? FALSE : 1;
        $this->last_page = ($this->current_page >= $this->total_pages) ? FALSE : $this->total_pages;
        $this->offset = (int)(($this->current_page - 1) * $this->items_per_page);

        // Chainable method
        return $this;
    }

    /**
     * Generates the full URL for a certain page.
     *
     * @param   integer  page number
     * @return  string   page URL
     */
    public function url($page = 1) {
        // Clean the page number
        $page = max(1, (int)$page);

        // No page number in URLs to first page
        if ($page === 1 AND !$this->first_page_in_url) {
            $page = NULL;
        }

        switch ($this->current_page_source) {
            case 'query_string':
            case 'mixed':

                return URL::site($this->request->uri() .
                    $this->query([$this->current_page_source_key => $page]));

            case 'route':

                return URL::site($this->route->url(array_merge($this->route_params,
                        array($this->current_page_source_key => $page))) . $this->query());
        }

        return '#';
    }

    /**
     * Checks whether the given page number exists.
     *
     * @param   integer $page page number
     * @return  boolean
     */
    public function valid_page(int $page): bool {
        return $page > 0 AND $page <= $this->total_pages;
    }

    /**
     * Renders the pagination links.
     *
     * @param   mixed   string of the block name to use, or a block object
     * @return  string  pagination output (HTML)
     */
    public function render($block = null) {
        // Automatically hide pagination whenever it is superfluous
        if ($this->auto_hide === true AND $this->total_pages <= 1)
            return '';

        if ($block === null) {
            // Use the view from config
            $block = $this->block;
        }

        if (!$block instanceof Block) {
            // Load the view file
            $block = block($block);
        }

        // Pass on the whole Pagination object
        return $block->set(get_object_vars($this))->set('page', $this)->render();
    }

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

    public function get_limit() {
        return $this->items_per_page;
    }

    public function next_page() {
        return $this->next_page;
    }

    public function prev_page() {
        return $this->previous_page;
    }

    public function links(): array {
        $links = [];
        if ($this->next_page()) {
            $links[] = [
                'rel' => 'next',
                'href' => $this->url($this->next_page())
            ];
        }

        if ($this->prev_page()) {
            $links[] = [
                'rel' => 'prev',
                'href' => $this->url($this->prev_page())
            ];
        }
        return [];
    }

    /**
     * URL::query() replacement for Pagination use only
     *
     * @param    array    Parameters to override
     * @return    string
     */
    public function query(array $params = NULL) {
        if ($params === NULL) {
            // Use only the current parameters
            $params = $this->request->get();
        } else {
            // Merge the current and new parameters
            $params = array_merge($this->request->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);
    }

    /**
     * Renders the pagination links.
     *
     * @return  string  pagination output (HTML)
     */
    public function __toString() {
        try {
            return $this->render();
        } catch (\Throwable $e) {
            ErrorHandler::convert_to_error($e);
            return '';
        }
    }

    /**
     * Returns a Pagination property.
     *
     * @param   string $key property name
     * @return  mixed   Pagination property; NULL if not found
     */
    public function __get(string $key) {
        return isset($this->$key) ? $this->$key : NULL;
    }

    /**
     * Updates a single config setting, and recalculates pagination if needed.
     *
     * @param   string $key config key
     * @param   mixed $value config value
     * @return  void
     */
    public function __set(string $key, $value) {
        $this->setup(array($key => $value));
    }

} // End Pagination


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

namespace Inject;

use Closure;
class Chains implements ChainsInterface
{

    /** @var  \Inject\InjectorInterface $context */
    protected $context;

    protected $handlers;

    protected $action;

    protected $req;

    public function __construct($context)
    {
        $this->context = $context;
        $this->handlers = [];
        $this->action = 'handle';
    }

    public function data($data){
        $this->req = $data;
        return $this;
    }

    public function chain($handlers = array()){
        $this->handlers = array_merge($this->handlers,is_array($handlers) ? $handlers : func_get_args());
        return $this;
    }

    public function action($action){
        $this->action = $action;
        return $this;
    }

    /***** run/runWith *****/
    // when use run/runWith,handler should be like "func($data,$next)" and return "$next($data)"

    public function run(){
        $handlers_registered = $this->handlers;
        $last_handler = array_pop($handlers_registered);
        $last = function($data) use ($last_handler){
            if($last_handler instanceof Closure){
                return call_user_func($last_handler,$data);
            }elseif (!is_object($last_handler)){
                $last_handler = $this->context->produce($last_handler);
            }
            $args = [$data];
            return call_user_func_array([$last_handler,$this->action],$args);
        };
        $handlers = array_reverse($handlers_registered);
        return call_user_func(array_reduce($handlers,$this->walk(),$last),$this->req);
    }

    public function runWith(Closure $call_back){
        $last = function($data) use ($call_back){
            return call_user_func($call_back,$data);
        };
        $handlers = array_reverse($this->handlers);
        return call_user_func(array_reduce($handlers,$this->walk(),$last),$this->req);
    }

    protected function walk(){
        return function($next_cb,$func_now){
            return function($data) use ($next_cb,$func_now){
                if($func_now instanceof Closure){
                    return call_user_func($func_now,$data,$next_cb);
                }elseif (!is_object($func_now)){
                    $func_now = $this->context->produce($func_now);
                }
                $args = [$data,$next_cb];
                return call_user_func_array([$func_now,$this->action],$args);
            };
        };
    }

    /***** runWild *****/
    // when use runWild , any handler return anything will break the loop and return

    public function runWild(){
        foreach ($this->handlers as $handler){
            $rtn = $handler instanceof Closure ? $this->context->call($handler,$this->req) : $this->context->callInClass($handler,$this->action,$this->req);
            if (!is_null($rtn)) return $rtn;
        }
        return null;
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentLabelInterface;

/**
 * Class Notify
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class Notify
{
    // (Used in twig template)
    const VIEW_NONE   = 'none';
    const VIEW_BEFORE = 'before';
    const VIEW_AFTER  = 'after';

    /**
     * @var string
     */
    private $type;

    /**
     * @var mixed
     */
    private $source;

    /**
     * @var Recipient
     */
    private $from;

    /**
     * @var ArrayCollection
     */
    private $recipients;

    /**
     * @var ArrayCollection
     */
    private $extraRecipients;

    /**
     * @var ArrayCollection
     */
    private $copies;

    /**
     * @var ArrayCollection
     */
    private $extraCopies;

    /**
     * @var ArrayCollection|InvoiceInterface[]
     */
    private $invoices;

    /**
     * @var ArrayCollection|ShipmentInterface[]
     */
    private $shipments;

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

    /**
     * @var ArrayCollection|AttachmentInterface[]
     */
    private $attachments;

    /**
     * @var string
     */
    private $subject;

    /**
     * @var string
     */
    private $paymentMessage;

    /**
     * @var string
     */
    private $shipmentMessage;

    /**
     * @var string
     */
    private $customMessage;

    /**
     * @var string
     */
    private $includeView;

    /**
     * (For supplier order)
     * @var bool
     */
    private $includeForm;

    /**
     * @var string
     */
    private $report;

    /**
     * @var boolean
     */
    private $test;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->type = NotificationTypes::MANUAL;

        $this->recipients = new ArrayCollection();
        $this->extraRecipients = new ArrayCollection();
        $this->copies = new ArrayCollection();
        $this->extraCopies = new ArrayCollection();
        $this->invoices = new ArrayCollection();
        $this->shipments = new ArrayCollection();
        $this->labels = new ArrayCollection();
        $this->attachments = new ArrayCollection();

        $this->includeView = static::VIEW_NONE;
        $this->test = false;
    }

    /**
     * Returns the source.
     *
     * @return mixed
     */
    public function getSource()
    {
        return $this->source;
    }

    /**
     * Sets the source.
     *
     * @param mixed $source
     *
     * @return Notify
     */
    public function setSource($source)
    {
        $this->source = $source;

        return $this;
    }

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

    /**
     * Sets the type.
     *
     * @param string $type
     *
     * @return Notify
     */
    public function setType($type)
    {
        NotificationTypes::isValidType($type);

        $this->type = $type;

        return $this;
    }

    /**
     * Returns the from.
     *
     * @return Recipient
     */
    public function getFrom()
    {
        return $this->from;
    }

    /**
     * Sets the from.
     *
     * @param Recipient $from
     *
     * @return Notify
     */
    public function setFrom(Recipient $from)
    {
        $this->from = $from;

        return $this;
    }

    /**
     * Returns the recipients.
     *
     * @return ArrayCollection|Recipient[]
     */
    public function getRecipients()
    {
        return $this->recipients;
    }

    /**
     * Adds the recipient.
     *
     * @param Recipient $recipient
     *
     * @return Notify
     */
    public function addRecipient(Recipient $recipient)
    {
        if (!$this->recipients->contains($recipient)) {
            $this->recipients->add($recipient);
        }

        return $this;
    }

    /**
     * Removes the recipient.
     *
     * @param Recipient $recipient
     *
     * @return Notify
     */
    public function removeRecipient(Recipient $recipient)
    {
        if ($this->recipients->contains($recipient)) {
            $this->recipients->removeElement($recipient);
        }

        return $this;
    }

    /**
     * Clears the recipients.
     *
     * @return Notify
     */
    public function clearRecipients()
    {
        $this->recipients = new ArrayCollection();

        return $this;
    }

    /**
     * Returns the extra recipients.
     *
     * @return ArrayCollection|Recipient[]
     */
    public function getExtraRecipients()
    {
        return $this->extraRecipients;
    }

    /**
     * Adds the extra recipient.
     *
     * @param Recipient $recipient
     *
     * @return Notify
     */
    public function addExtraRecipient(Recipient $recipient)
    {
        if (!$this->extraRecipients->contains($recipient)) {
            $this->extraRecipients->add($recipient);
        }

        return $this;
    }

    /**
     * Removes the extra recipient.
     *
     * @param Recipient $recipient
     *
     * @return Notify
     */
    public function removeExtraRecipient(Recipient $recipient)
    {
        if ($this->extraRecipients->contains($recipient)) {
            $this->extraRecipients->removeElement($recipient);
        }

        return $this;
    }

    /**
     * Clears the extra recipients.
     *
     * @return Notify
     */
    public function clearExtraRecipients()
    {
        $this->extraRecipients = new ArrayCollection();

        return $this;
    }

    /**
     * Returns the copies.
     *
     * @return ArrayCollection|Recipient
     */
    public function getCopies()
    {
        return $this->copies;
    }

    /**
     * Adds the copy.
     *
     * @param Recipient $copy
     *
     * @return Notify
     */
    public function addCopy(Recipient $copy)
    {
        if (!$this->copies->contains($copy)) {
            $this->copies->add($copy);
        }

        return $this;
    }

    /**
     * Removes the copy.
     *
     * @param Recipient $copy
     *
     * @return Notify
     */
    public function removeCopy(Recipient $copy)
    {
        if ($this->copies->contains($copy)) {
            $this->copies->removeElement($copy);
        }

        return $this;
    }

    /**
     * Clears the copies.
     *
     * @return Notify
     */
    public function clearCopies()
    {
        $this->copies = new ArrayCollection();

        return $this;
    }

    /**
     * Returns the extra copies.
     *
     * @return ArrayCollection|Recipient
     */
    public function getExtraCopies()
    {
        return $this->extraCopies;
    }

    /**
     * Adds the extra copy.
     *
     * @param Recipient $copy
     *
     * @return Notify
     */
    public function addExtraCopy(Recipient $copy)
    {
        if (!$this->extraCopies->contains($copy)) {
            $this->extraCopies->add($copy);
        }

        return $this;
    }

    /**
     * Removes the extra copy.
     *
     * @param Recipient $copy
     *
     * @return Notify
     */
    public function removeExtraCopy(Recipient $copy)
    {
        if ($this->extraCopies->contains($copy)) {
            $this->extraCopies->removeElement($copy);
        }

        return $this;
    }

    /**
     * Clears the extra copies.
     *
     * @return Notify
     */
    public function clearExtraCopies()
    {
        $this->extraCopies = new ArrayCollection();

        return $this;
    }

    /**
     * Returns the invoices.
     *
     * @return array|InvoiceInterface[]
     */
    public function getInvoices()
    {
        return $this->invoices;
    }

    /**
     * Adds the invoice.
     *
     * @param InvoiceInterface $invoice
     *
     * @return Notify
     */
    public function addInvoice(InvoiceInterface $invoice)
    {
        if (!$this->invoices->contains($invoice)) {
            $this->invoices->add($invoice);
        }

        return $this;
    }

    /**
     * Removes the invoice.
     *
     * @param InvoiceInterface $invoice
     *
     * @return Notify
     */
    public function removeInvoice(InvoiceInterface $invoice)
    {
        if ($this->invoices->contains($invoice)) {
            $this->invoices->removeElement($invoice);
        }

        return $this;
    }

    /**
     * Returns the shipments.
     *
     * @return ArrayCollection|ShipmentInterface[]
     */
    public function getShipments()
    {
        return $this->shipments;
    }

    /**
     * Adds the shipment.
     *
     * @param ShipmentInterface $shipment
     *
     * @return Notify
     */
    public function addShipment(ShipmentInterface $shipment)
    {
        if (!$this->shipments->contains($shipment)) {
            $this->shipments->add($shipment);
        }

        return $this;
    }

    /**
     * Removes the shipment.
     *
     * @param ShipmentInterface $shipment
     *
     * @return Notify
     */
    public function removeShipment(ShipmentInterface $shipment)
    {
        if ($this->shipments->contains($shipment)) {
            $this->shipments->removeElement($shipment);
        }

        return $this;
    }

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

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

        return $this;
    }

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

        return $this;
    }

    /**
     * Returns the attachments.
     *
     * @return ArrayCollection|AttachmentInterface[]
     */
    public function getAttachments()
    {
        return $this->attachments;
    }

    /**
     * Adds the attachment.
     *
     * @param AttachmentInterface $attachment
     *
     * @return Notify
     */
    public function addAttachment(AttachmentInterface $attachment)
    {
        if (!$this->attachments->contains($attachment)) {
            $this->attachments->add($attachment);
        }

        return $this;
    }

    /**
     * Removes the attachment.
     *
     * @param AttachmentInterface $attachment
     *
     * @return Notify
     */
    public function removeAttachment(AttachmentInterface $attachment)
    {
        if ($this->attachments->contains($attachment)) {
            $this->attachments->removeElement($attachment);
        }

        return $this;
    }

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

    /**
     * Sets the subject.
     *
     * @param string $subject
     *
     * @return Notify
     */
    public function setSubject($subject)
    {
        $this->subject = $subject;

        return $this;
    }

    /**
     * Returns the payment message.
     *
     * @return string
     */
    public function getPaymentMessage()
    {
        return $this->paymentMessage;
    }

    /**
     * Sets the payment message.
     *
     * @param string $paymentMessage
     *
     * @return Notify
     */
    public function setPaymentMessage($paymentMessage)
    {
        $this->paymentMessage = $paymentMessage;

        return $this;
    }

    /**
     * Returns the shipment message.
     *
     * @return string
     */
    public function getShipmentMessage()
    {
        return $this->shipmentMessage;
    }

    /**
     * Sets the shipment message.
     *
     * @param string $message
     *
     * @return Notify
     */
    public function setShipmentMessage($message = null)
    {
        $this->shipmentMessage = $message;

        return $this;
    }

    /**
     * Returns the custom message.
     *
     * @return string
     */
    public function getCustomMessage()
    {
        return $this->customMessage;
    }

    /**
     * Sets the custom message.
     *
     * @param string $customMessage
     *
     * @return Notify
     */
    public function setCustomMessage($customMessage)
    {
        $this->customMessage = $customMessage;

        return $this;
    }

    /**
     * Returns the include view.
     *
     * @return string
     */
    public function getIncludeView()
    {
        return $this->includeView;
    }

    /**
     * Sets the include view.
     *
     * @param string $include
     *
     * @return Notify
     */
    public function setIncludeView($include)
    {
        $this->includeView = $include;

        return $this;
    }

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

    /**
     * Sets the includeForm.
     *
     * @param bool $include
     *
     * @return Notify
     */
    public function setIncludeForm($include)
    {
        $this->includeForm = $include;

        return $this;
    }

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

    /**
     * Sets the report.
     *
     * @param string $report
     *
     * @return Notify
     */
    public function setReport($report)
    {
        $this->report = $report;

        return $this;
    }

    /**
     * Returns whether this is a test.
     *
     * @return bool
     */
    public function isTest()
    {
        return $this->test;
    }

    /**
     * Sets whether this is a test.
     *
     * @param bool $test
     *
     * @return Notify
     */
    public function setTest($test)
    {
        $this->test = $test;

        return $this;
    }

    /**
     * Returns whether there is no defined message.
     *
     * @return bool
     */
    public function isEmpty()
    {
        return empty($this->subject)
            || (empty($this->customMessage) && empty($this->paymentMessage) && empty($this->shipmentMessage));
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Model;

use Doctrine\Common\Collections\ArrayCollection;

/**
 * Trait AdjustableTrait
 * @package Ekyna\Component\Commerce\Common\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
trait AdjustableTrait
{
    /**
     * @var ArrayCollection|AdjustmentInterface[]
     */
    protected $adjustments;

    /**
     * Initializes the adjustments.
     */
    protected function initializeAdjustments()
    {
        $this->adjustments = new ArrayCollection();
    }

    /**
     * Returns whether the adjustable has adjustments or not, optionally filtered by type.
     *
     * @param string $type
     *
     * @return bool
     */
    public function hasAdjustments($type = null)
    {
        if (null !== $type) {
            AdjustmentTypes::isValidType($type);

            return $this->getAdjustments($type)->count();
        }

        return 0 < $this->adjustments->count();
    }

    /**
     * Returns the adjustments, optionally filtered by type.
     *
     * @param string $type
     *
     * @return ArrayCollection|AdjustmentInterface[]
     */
    public function getAdjustments($type = null)
    {
        if (null !== $type) {
            AdjustmentTypes::isValidType($type);

            return $this
                ->adjustments
                ->filter(function (AdjustmentInterface $a) use ($type) {
                    return $a->getType() === $type;
                });
        }

        return $this->adjustments;
    }
}


<?php

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

use Ekyna\Component\Commerce\Common\Model\IdentityInterface;
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\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

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

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

        if (!$identity instanceof IdentityInterface) {
            throw new UnexpectedTypeException($constraint, IdentityInterface::class);
        }
        if (!$constraint instanceof Identity) {
            throw new UnexpectedTypeException($constraint, Identity::class);
        }

        // All or none
        $gender = $identity->getGender();
        $firstName = $identity->getFirstName();
        $lastName = $identity->getLastName();

        $all = $gender . $lastName . $firstName;

        if (empty($all)) {
            if ($constraint->required) {
                $this->context
                    ->buildViolation($constraint->mandatory)
                    ->atPath('gender')
                    ->addViolation();
            }
        } else {
            $config = [
                'gender'    => [
                    new Assert\NotBlank(['message' => $constraint->gender_is_mandatory]),
                    new Gender(),
                ],
                'firstName' => [
                    new Assert\NotBlank(['message' => $constraint->first_name_is_mandatory]),
                    new Assert\Length(['min' => 2, 'max' => 32,]),
                ],
                'lastName'  => [
                    new Assert\NotBlank(['message' => $constraint->last_name_is_mandatory]),
                    new Assert\Length(['min' => 2, 'max' => 32,]),
                ],
            ];

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

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

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

    /**
     * Validates the identity for the given context.
     *
     * @param ExecutionContextInterface $context
     * @param IdentityInterface         $identity
     * @param array                     $config
     * @param string                    $pathPrefix
     */
    static public function validateIdentity(
        ExecutionContextInterface $context,
        IdentityInterface $identity,
        array $config = [],
        $pathPrefix = null
    ) {
        $violationList = $context->getValidator()->validate($identity, [new Identity($config)]);

        if (!empty($pathPrefix)) {
            $pathPrefix = rtrim($pathPrefix, '.') . '.';
        }

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


<?php

namespace Ekyna\Component\Commerce\Pricing\Resolver;

use Ekyna\Component\Commerce\Common\Model\CountryInterface;
use Ekyna\Component\Commerce\Pricing\Model\TaxGroupInterface;
use Ekyna\Component\Commerce\Pricing\Model\TaxInterface;

/**
 * Class ResolvedTaxesCache
 * @package Ekyna\Component\Commerce\Pricing\Resolver
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ResolvedTaxesCache
{
    /**
     * @var array
     */
    private $taxes;

    /**
     * Returns the taxes for the given tax group, country and business flag.
     *
     * @param TaxGroupInterface $taxGroup
     * @param CountryInterface  $country
     * @param bool              $business
     *
     * @return TaxInterface[]|null
     */
    public function get(TaxGroupInterface $taxGroup, CountryInterface $country, $business = false)
    {
        $key = $this->buildKey($taxGroup, $country, $business);

        if (isset($this->taxes[$key])) {
            return $this->taxes[$key];
        }

        return null;
    }

    /**
     * Caches the taxes for the given tax group, country and business flag.
     *
     * @param TaxGroupInterface $taxGroup
     * @param CountryInterface  $country
     * @param bool              $business
     * @param array             $taxes
     */
    public function set(TaxGroupInterface $taxGroup, CountryInterface $country, $business = false, array $taxes)
    {
        $key = $this->buildKey($taxGroup, $country, $business);

        $this->taxes[$key] = $taxes;
    }

    /**
     * Builds the cache key.
     *
     * @param TaxGroupInterface $taxGroup
     * @param CountryInterface  $country
     * @param bool              $business
     *
     * @return string
     */
    private function buildKey(TaxGroupInterface $taxGroup, CountryInterface $country, $business = false)
    {
        return sprintf('%s-%s-%s', $taxGroup->getId(), $country->getId(), (int)$business);
    }
}


<?php

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

use Ekyna\Component\Commerce\Customer\Model\CustomerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\InvalidArgumentException;

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

        if (!$customer instanceof CustomerInterface) {
            throw new InvalidArgumentException('Expected instance of CustomerInterface');
        }
        if (!$constraint instanceof Customer) {
            throw new InvalidArgumentException('Expected instance of Customer (validation constraint)');
        }

        /* @var CustomerInterface $customer */
        /* @var Customer $constraint */

        // Prevent setting a parent to a customer that is already a parent (has children)
        if ($customer->hasParent()) {
            if ($customer->hasChildren() || $customer->getParent()->hasParent()) {
                $this
                    ->context
                    ->buildViolation($constraint->hierarchy_overflow)
                    ->atPath('parent')
                    ->addViolation();

                return;
            }

            // Prevent setting a parent to a customer who have non zero outstanding|credit balance
            if (0 != $customer->getOutstandingBalance() || 0 != $customer->getCreditBalance()) {
                $this
                    ->context
                    ->buildViolation($constraint->non_zero_balance)
                    ->atPath('parent')
                    ->addViolation();

                return;
            }
        }

        // A parent must have a company name.
        if ($customer->hasParent() && 0 == strlen($customer->getParent()->getCompany())) {
            $this
                ->context
                ->buildViolation($constraint->parent_company_is_mandatory)
                ->atPath('parent')
                ->addViolation();

            return;
        } elseif ($customer->hasChildren() && 0 == strlen($customer->getCompany())) {
            $this
                ->context
                ->buildViolation($constraint->company_is_mandatory)
                ->atPath('company')
                ->addViolation();

            return;
        }

        // Outstanding / Payment term
        $hasOutstanding = 0 < $customer->getOutstandingLimit();
        $hasPaymentTerm = null !== $customer->getPaymentTerm();
        if ($hasOutstanding && !$hasPaymentTerm) {
            $this
                ->context
                ->buildViolation($constraint->term_required_for_outstanding)
                ->atPath('paymentTerm')
                ->addViolation();

            return;
        } else if ($hasPaymentTerm && !$hasOutstanding) {
            $this
                ->context
                ->buildViolation($constraint->outstanding_required_for_term)
                ->atPath('outstandingLimit')
                ->addViolation();

            return;
        }
    }
}


<?php

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

use Doctrine\DBAL\Types\Type;
use Ekyna\Component\Commerce\Common\Calculator\AmountCalculatorInterface;
use Ekyna\Component\Commerce\Common\Context\ContextProviderInterface;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Pricing\Model\VatDisplayModes;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentMethodInterface;
use Ekyna\Component\Commerce\Shipment\Repository\ShipmentRuleRepositoryInterface;
use Ekyna\Component\Resource\Doctrine\ORM\ResourceRepository;

/**
 * Class ShipmentRuleRepository
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Repository
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ShipmentRuleRepository extends ResourceRepository implements ShipmentRuleRepositoryInterface
{
    /**
     * @var ContextProviderInterface
     */
    protected $contextProvider;

    /**
     * @var AmountCalculatorInterface
     */
    protected $amountCalculator;

    /**
     * @var \Doctrine\ORM\Query
     */
    protected $findOneBySaleQuery;

    /**
     * Sets the context provider.
     *
     * @param ContextProviderInterface $provider
     */
    public function setContextProvider(ContextProviderInterface $provider)
    {
        $this->contextProvider = $provider;
    }

    /**
     * Sets the amount calculator.
     *
     * @param AmountCalculatorInterface $calculator
     */
    public function setAmountCalculator(AmountCalculatorInterface $calculator)
    {
        $this->amountCalculator = $calculator;
    }

    /**
     * @inheritdoc
     */
    public function findOneBySale(SaleInterface $sale, ShipmentMethodInterface $method = null)
    {
        // Use sale's shipment method if not passed
        if (null === $method) {
            $method = $sale->getShipmentMethod();
        }
        if (null === $method) {
            return null;
        }

        $context = $this->contextProvider->getContext($sale);

        $result = $this->amountCalculator->calculateSaleItems($sale);

        $parameters = [
            'net_mode' => VatDisplayModes::MODE_NET,
            'net_base' => $result->getBase(),
            'ati_mode' => VatDisplayModes::MODE_ATI,
            'ati_base' => $result->getBase(true),
            'method'   => $method,
            'country'  => $context->getDeliveryCountry(),
            'group'    => $context->getCustomerGroup(),
        ];

        return $this
            ->getFindOneBySaleQuery()
            ->setParameters($parameters)
            ->setParameter('date', $context->getDate(), Type::DATE)
            ->getOneOrNullResult();
    }

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

        $qb = $this->createQueryBuilder('r');
        $e = $qb->expr();

        return $this->findOneBySaleQuery = $qb
            ->andWhere($e->orX(
                $e->andX(
                    $e->eq('r.vatMode', ':net_mode'),
                    $e->lte('r.baseTotal', ':net_base')
                ),
                $e->andX(
                    $e->eq('r.vatMode', ':ati_mode'),
                    $e->lte('r.baseTotal', ':ati_base')
                )
            ))
            ->andWhere($e->orX(
                'r.methods IS EMPTY',
                $e->isMemberOf(':method', 'r.methods')
            ))
            ->andWhere($e->orX(
                'r.countries IS EMPTY',
                $e->isMemberOf(':country', 'r.countries')
            ))
            ->andWhere($e->orX(
                'r.customerGroups IS EMPTY',
                $e->isMemberOf(':group', 'r.customerGroups')
            ))
            ->andWhere($e->orX(
                'r.startAt IS NULL',
                $e->lte('r.startAt', ':date')
            ))
            ->andWhere($e->orX(
                'r.endAt IS NULL',
                $e->gte('r.endAt', ':date')
            ))
            ->getQuery()
            ->setMaxResults(1)
            ->useQueryCache(true);
    }
}


<?php

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

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

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

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

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

        $cancellableStates = [Constants::STATUS_CAPTURED, Constants::STATUS_AUTHORIZED];

        if (in_array($model[Constants::FIELD_STATUS], $cancellableStates, true)) {
            $model[Constants::FIELD_STATUS] = Constants::STATUS_CANCELED;
        }
    }

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


<?php

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

use Ekyna\Component\Commerce\Common\Model\CountryInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentMethodInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentPriceInterface;
use Ekyna\Component\Commerce\Shipment\Repository\ShipmentPriceRepositoryInterface;
use Ekyna\Component\Resource\Doctrine\ORM\ResourceRepository;

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

    /**
     * @inheritdoc
     */
    public function findOneByCountryAndMethodAndWeight(
        CountryInterface $country,
        ShipmentMethodInterface $method,
        $weight
    ) {
        return $this
            ->getFindOneByCountryAndMethodAndWeightQuery()
            ->setParameters([
                'country' => $country,
                'method'  => $method,
                'weight'  => $weight,
//                'enabled'   => true,
//                'available' => true,
            ])
            ->getOneOrNullResult();
    }

    /**
     * @inheritdoc
     */
    public function findByCountryAndWeight(CountryInterface $country, $weight, $available = true)
    {
        $qb = $this->getCollectionQueryBuilder('o');
        $qb
            ->join('o.zone', 'z')
            ->join('o.method', 'm')
            ->andWhere($qb->expr()->isMemberOf(':country', 'z.countries'))
            ->andWhere($qb->expr()->gte('o.weight', ':weight'))
            ->andWhere($qb->expr()->eq('m.enabled', ':enabled'))
            ->addOrderBy('o.weight', 'ASC');

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

        if ($available) {
            $qb->andWhere($qb->expr()->eq('m.available', ':available'));

            $parameters['available'] = true;
        }

        $prices = $qb
            ->getQuery()
            ->setParameters($parameters)
            ->getResult();

        $result = [];
        $methodIds = [];

        /** @var ShipmentPriceInterface $price */
        foreach ($prices as $price) {
            $methodId = intval($price->getMethod()->getId());

            if (in_array($methodId, $methodIds, true)) {
                continue;
            }

            $methodIds[] = $methodId;
            $result[] = $price;
        }

        usort($result, function (ShipmentPriceInterface $a, ShipmentPriceInterface $b) {
            $aPos = $a->getMethod()->getPosition();
            $bPos = $b->getMethod()->getPosition();

            if ($aPos == $bPos) {
                return 0;
            }

            return $aPos > $bPos ? 1 : -1;
        });

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function findByCountry(CountryInterface $country)
    {
        $qb = $this->getCollectionQueryBuilder('o');
        $qb
            ->join('o.zone', 'z')
            ->join('o.method', 'm')
            ->andWhere($qb->expr()->isMemberOf(':country', 'z.countries'))
            ->andWhere($qb->expr()->eq('m.enabled', ':enabled'))
            ->addOrderBy('m.position', 'ASC')
            ->addOrderBy('o.weight', 'DESC');

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

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

    /**
     * Returns the "find one price by country, method and weight" query.
     *
     * @return \Doctrine\ORM\Query
     */
    private function getFindOneByCountryAndMethodAndWeightQuery()
    {
        if (null === $this->findOneByCountryAndMethodAndWeightQuery) {
            $qb = $this->getCollectionQueryBuilder('o');
            $qb
                ->join('o.zone', 'z')
                ->join('o.method', 'm')
                ->andWhere($qb->expr()->isMemberOf(':country', 'z.countries'))
                ->andWhere($qb->expr()->gte('o.weight', ':weight'))
                ->andWhere($qb->expr()->eq('o.method', ':method'))
                ->addOrderBy('o.weight', 'ASC')
                ->setMaxResults(1)
            ;

            $this->findOneByCountryAndMethodAndWeightQuery = $qb->getQuery();
        }

        return $this->findOneByCountryAndMethodAndWeightQuery;
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Listener;

use Ekyna\Component\Commerce\Common\Notify\NotifyBuilder;
use Ekyna\Component\Commerce\Common\Notify\NotifyQueue;
use Ekyna\Component\Resource\Persistence\PersistenceTrackerInterface;

/**
 * Class AbstractNotifyListener
 * @package Ekyna\Component\Commerce\Common\Listener
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
abstract class AbstractNotifyListener
{
    /**
     * @var PersistenceTrackerInterface
     */
    protected $tracker;

    /**
     * @var NotifyQueue
     */
    protected $queue;

    /**
     * @var NotifyBuilder
     */
    protected $builder;

    /**
     * Constructor.
     *
     * @param PersistenceTrackerInterface $tracker
     * @param NotifyQueue                 $queue
     * @param NotifyBuilder               $builder
     */
    public function __construct(
        PersistenceTrackerInterface $tracker,
        NotifyQueue $queue,
        NotifyBuilder $builder
    ) {
        $this->tracker = $tracker;
        $this->queue = $queue;
        $this->builder = $builder;
    }

    /**
     * Returns whether the state of the given resource changed to the given state.
     *
     * @param object $resource
     * @param string $state
     *
     * @return bool
     */
    protected function didStateChangeTo($resource, $state)
    {
        if (empty($stateCs = $this->tracker->getChangeSet($resource, 'state'))) {
            return false;
        }

        if ($stateCs[1] === $state && $stateCs[0] !== $state) {
            return true;
        }

        return false;
    }

    /**
     * Creates, build and enqueue a notify instance.
     *
     * @param string $type
     * @param object $resource
     */
    protected function notify($type, $resource)
    {
        // Create
        $notify = $this->builder->create($type, $resource);

        // Build
        if (!$this->builder->build($notify)) {
            return;
        }

        // Enqueue
        $this->queue->add($notify);
    }
}


<?php

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

use Ekyna\Component\Commerce\Supplier\Model\SupplierOrderInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

/**
 * Class SupplierOrderNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SupplierOrderNormalizer extends AbstractResourceNormalizer
{
    /**
     * @inheritdoc
     *
     * @param SupplierOrderInterface $order
     */
    public function normalize($order, $format = null, array $context = [])
    {
        $data = parent::normalize($order, $format, $context);

        if ($this->contextHasGroup('Summary', $context)) {
            $items = [];
            foreach ($order->getItems() as $item) {
                $items[] = $this->normalizeObject($item, $format, $context);
            }

            $data = array_replace($data, [
                'items'       => $items,
                'description' => $order->getDescription(),
            ]);
        }

        return $data;
    }

    /**
     * @inheritdoc
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        //$object = parent::denormalize($data, $class, $format, $context);

        throw new \Exception('Not yet implemented');
    }

    /**
     * @inheritdoc
     */
    public function supportsNormalization($data, $format = null)
    {
        return $data instanceof SupplierOrderInterface;
    }

    /**
     * @inheritdoc
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return class_exists($type) && is_subclass_of($type, SupplierOrderInterface::class);
    }
}


<?php

namespace Skriv\Markup\DocBook;

/**
 * traite les signes de type paragraphe
 */
class Paragraph extends \WikiRenderer\Block {
	public $type = 'para';
	protected $_openTag = '<para>';
	protected $_closeTag = '</para>';
	// attribut utilisé pour gérer les retours charriots dans les paragraphes
	private $_firstLine = true;

	/**
	 * Détection des paragraphes
	 * @param	string	$string		Ligne de texte servant pour la détection.
	 * @param	bool	$inBlock	(optional) True is the parser is already in the block.
	 * @return	bool	True si c'est un paragraphe.
	 */
	public function detect($string, $inBlock=false) {
		if (empty($string))
			return (false);
		if (!preg_match("/^\s*\*{2}.*\*{2}\s*.*$/", $string) &&
		    !preg_match("/^\s*#{2}.*#{2}\s*.*$/", $string) &&
		    preg_match("/^\s*[\*#\-\!\| \t>;<=].*/", $string))
			return (false);
		$this->_detectMatch = array($string, $string);
		return (true);
	}
}


<?php

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

use Ekyna\Component\Commerce\Invoice\Model\InvoiceLineInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

/**
 * Class InvoiceItemNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class InvoiceLineNormalizer extends AbstractResourceNormalizer
{
    /**
     * @inheritdoc
     *
     * @param InvoiceLineInterface $line
     */
    public function normalize($line, $format = null, array $context = [])
    {
        $data = [];

        if ($this->contextHasGroup('Summary', $context)) {
            $saleItem = $line->getSaleItem();

            $data = array_replace($data, [
                'designation' => $line->getDesignation(),
                'reference'   => $line->getReference(),
                'quantity'    => $line->getQuantity(),
                'total'       => $saleItem ? $saleItem->getTotalQuantity() : null,
            ]);
        }

        return $data;
    }

    /**
     * @inheritdoc
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        //$object = parent::denormalize($data, $class, $format, $context);

        throw new \Exception('Not yet implemented');
    }

    /**
     * @inheritdoc
     */
    public function supportsNormalization($data, $format = null)
    {
        return $data instanceof InvoiceLineInterface;
    }

    /**
     * @inheritdoc
     */
    public function supportsDenormalization($data, $type, $format = null)
    {
        return class_exists($type) && is_subclass_of($type, InvoiceLineInterface::class);
    }
}