<?php

namespace Ekyna\Component\Commerce\Cart\Entity;

use Ekyna\Component\Commerce\Common\Entity\AbstractSaleItem;
use Ekyna\Component\Commerce\Common\Model as Common;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Cart\Model;

/**
 * Class CartItem
 * @package Ekyna\Component\Commerce\Cart\Entity
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class CartItem extends AbstractSaleItem implements Model\CartItemInterface
{
    /**
     * @var Model\CartInterface
     */
    protected $cart;

    /**
     * @inheritdoc
     */
    public function getSale()
    {
        if (null === $cart = $this->getCart()) {
            $parent = $this;
            while (null !== $parent) {
                if (null !== $cart = $parent->getCart()) {
                    return $cart;
                }
                $parent = $parent->getParent();
            }
        }

        return $cart;
    }

    /**
     * @inheritdoc
     *
     * @param Model\CartInterface $sale
     */
    public function setSale(Common\SaleInterface $sale = null)
    {
        $sale && $this->assertSaleClass($sale);

        $this->setCart($sale);

        return $this;
    }

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

    /**
     * @inheritdoc
     */
    public function setCart(Model\CartInterface $cart = null)
    {
        if ($cart !== $this->cart) {
            if ($previous = $this->cart) {
                $this->cart = null;
                $previous->removeItem($this);
            }

            if ($this->cart = $cart) {
                $this->cart->addItem($this);
            }
        }

        return $this;
    }

    /**
     * Asserts that the given sale is an instance of the expected class.
     *
     * @param Common\SaleInterface $sale
     */
    protected function assertSaleClass(Common\SaleInterface $sale)
    {
        if (!$sale instanceof Model\CartInterface) {
            throw new InvalidArgumentException("Expected instance of " . Model\CartInterface::class);
        }
    }

    /**
     * Asserts that the given sale item is an instance of the expected class.
     *
     * @param Common\SaleItemInterface $child
     */
    protected function assertItemClass(Common\SaleItemInterface $child)
    {
        if (!$child instanceof Model\CartItemInterface) {
            throw new InvalidArgumentException("Expected instance of " . Model\CartItemInterface::class);
        }
    }

    /**
     * Asserts that the given adjustment is an instance of the expected class.
     *
     * @param Common\AdjustmentInterface $adjustment
     */
    protected function assertItemAdjustmentClass(Common\AdjustmentInterface $adjustment)
    {
        if (!$adjustment instanceof Model\CartItemAdjustmentInterface) {
            throw new InvalidArgumentException("Expected instance of " . Model\CartItemAdjustmentInterface::class);
        }
    }
}


<?php

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

use Ekyna\Component\Commerce\Common\Model\AddressInterface;
use Ekyna\Component\Commerce\Customer\Model\CustomerAddressInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;
use libphonenumber\PhoneNumberUtil;
use libphonenumber\PhoneNumber;

/**
 * Class AddressNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class AddressNormalizer extends AbstractResourceNormalizer
{
    /**
     * @var PhoneNumberUtil
     */
    private $phoneNumberUtil;

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

    /**
     * @inheritdoc
     *
     * @param AddressInterface $address
     */
    public function normalize($address, $format = null, array $context = [])
    {
        $data = parent::normalize($address, $format, $context);

        $groups = [
            'Address',
            'CartAddress',
            'CustomerAddress',
            'Default',
            'OrderAddress',
            'QuoteAddress',
            'ShipmentAddress',
            'SupplierAddress',
        ];

        if ($this->contextHasGroup($groups, $context)) {
            $data = array_replace($data, [
                'company'        => $address->getCompany(),
                'gender'         => $address->getGender(),
                'first_name'     => $address->getFirstName(),
                'last_name'      => $address->getLastName(),
                'street'         => $address->getStreet(),
                'complement'     => $address->getComplement(),
                'supplement'     => $address->getSupplement(),
                'postal_code'    => $address->getPostalCode(),
                'city'           => $address->getCity(),
                'country_name'   => $address->getCountry()->getName(),
                'country'        => $address->getCountry()->getCode(),
                //'state'        => $address->getCity(),
                'phone'          => $this->normalizeObject($address->getPhone(), $format, $context),
                'phone_country'  => $this->phoneNumberCountry($address->getPhone()),
                'mobile'         => $this->normalizeObject($address->getMobile(), $format, $context),
                'mobile_country' => $this->phoneNumberCountry($address->getMobile()),
                'digicode1'      => $address->getDigicode1(),
                'digicode2'      => $address->getDigicode2(),
                'intercom'       => $address->getIntercom(),
            ]);

            if ($address instanceof CustomerAddressInterface) {
                $data['invoice_default'] = $address->isInvoiceDefault() ? 1 : 0;
                $data['delivery_default'] = $address->isDeliveryDefault() ? 1 : 0;
            }
        }

        return $data;
    }

    /**
     * Returns the region code for the given phone number.
     *
     * @param PhoneNumber|null $phoneNumber
     *
     * @return string|null
     */
    private function phoneNumberCountry(PhoneNumber $phoneNumber = null)
    {
        if ($phoneNumber) {
            return $this->phoneNumberUtil->getRegionCodeForNumber($phoneNumber);
        }

        return null;
    }

    /**
     * @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 AddressInterface;
    }

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


<?php

namespace Ekyna\Component\Commerce\Customer\EventListener;

use Ekyna\Component\Commerce\Customer\Model\CustomerAddressInterface;
use Ekyna\Component\Commerce\Exception\IllegalOperationException;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

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

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

    /**
     * Pre delete event handler.
     *
     * @param ResourceEventInterface $event
     *
     * @throws IllegalOperationException
     */
    public function onPreDelete(ResourceEventInterface $event)
    {
        $address = $this->getAddressFromEvent($event);

        if (null === $customer = $address->getCustomer()) {
            return;
        }

        if ($address->isInvoiceDefault()) {
            if (!($customer->hasParent() && null !== $customer->getParent()->getDefaultInvoiceAddress())) {
                throw new IllegalOperationException(); // TODO reason message
            }
        } elseif ($address->isDeliveryDefault()) {
            if (!($customer->hasParent() && null !== $customer->getParent()->getDefaultDeliveryAddress())) {
                throw new IllegalOperationException(); // TODO reason message
            }
        }
    }

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

        $this->fixInvoiceDefault($address);
        $this->fixDeliveryDefault($address);
    }

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

        $this->fixInvoiceDefault($address);
        $this->fixDeliveryDefault($address);
    }

    /**
     * Fixes the default invoice address.
     *
     * @param CustomerAddressInterface $address
     */
    protected function fixInvoiceDefault(CustomerAddressInterface $address)
    {
        if (!$this->persistenceHelper->isChanged($address, ['invoiceDefault'])) {
            return;
        }

        $customer = $address->getCustomer();

        if ($address->isInvoiceDefault()) {
            foreach ($customer->getAddresses() as $a) {
                if ($a === $address) {
                    continue;
                }

                if ($a->isInvoiceDefault()) {
                    $a->setInvoiceDefault(false);
                    $this->persistenceHelper->persistAndRecompute($a, false);
                }
            }
        } elseif (null === $customer->getDefaultInvoiceAddress(true)) {
            $address->setInvoiceDefault(true);
            $this->persistenceHelper->persistAndRecompute($address, false);
        }
    }

    /**
     * Fix the default delivery address.
     *
     * @param CustomerAddressInterface $address
     */
    protected function fixDeliveryDefault(CustomerAddressInterface $address)
    {
        if (!$this->persistenceHelper->isChanged($address, ['deliveryDefault'])) {
            return;
        }

        $customer = $address->getCustomer();

        if ($address->isDeliveryDefault()) {
            foreach ($customer->getAddresses() as $a) {
                if ($a === $address) {
                    continue;
                }

                if ($a->isDeliveryDefault()) {
                    $a->setDeliveryDefault(false);
                    $this->persistenceHelper->persistAndRecompute($a, false);
                }
            }
        } elseif (null === $customer->getDefaultDeliveryAddress(true)) {
            $address->setDeliveryDefault(true);
            $this->persistenceHelper->persistAndRecompute($address, false);
        }
    }

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

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

        return $resource;
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\Factory;

use Ekyna\Component\Commerce\Cart;
use Ekyna\Component\Commerce\Common\Model;
use Ekyna\Component\Commerce\Common\Util\AddressUtil;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Order;
use Ekyna\Component\Commerce\Quote;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentInterface;

/**
 * Class SaleFactory
 * @package Ekyna\Component\Commerce\Common
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleFactory implements SaleFactoryInterface
{
    /**
     * @var array
     */
    private $classes;

    /**
     * Constructor.
     *
     * @param array $classes
     */
    public function __construct(array $classes = [])
    {
        $this->classes = array_replace_recursive($this->getDefaultClasses(), $classes);

        // TODO validate classes
    }

    /**
     * @inheritdoc
     */
    public function createAddressForSale(Model\SaleInterface $sale, Model\AddressInterface $source = null)
    {
        /** @var Model\SaleAddressInterface $address */
        $address = $this->resolveClassAndCreateObject('address', $sale);

        if (null !== $source) {
            AddressUtil::copy($source, $address);
        }

        return $address;
    }

    /**
     * @inheritdoc
     */
    public function createAttachmentForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('attachment', $sale);
    }

    /**
     * @inheritdoc
     */
    public function createNotificationForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('notification', $sale);
    }

    /**
     * @inheritdoc
     */
    public function createAdjustmentFor(Model\AdjustableInterface $adjustable)
    {
        if ($adjustable instanceof Model\SaleInterface) {
            return $this->createAdjustmentForSale($adjustable);
        } elseif ($adjustable instanceof Model\SaleItemInterface) {
            return $this->createAdjustmentForItem($adjustable);
        }

        throw new InvalidArgumentException("Expected instanceof SaleInterface or SaleItemInterface.");
    }

    /**
     * @inheritdoc
     */
    public function createAdjustmentForItem(Model\SaleItemInterface $item)
    {
        return $this->resolveClassAndCreateObject('item_adjustment', $item);
    }

    /**
     * @inheritdoc
     */
    public function createStockAssignmentForItem(Model\SaleItemInterface $item)
    {
        return $this->resolveClassAndCreateObject('item_stock_assignment', $item);
    }

    /**
     * @inheritdoc
     */
    public function createAdjustmentForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('adjustment', $sale);
    }

    /**
     * @inheritdoc
     */
    public function createItemForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('item', $sale);
    }

    /**
     * @inheritdoc
     */
    public function createItemForShipment(ShipmentInterface $shipment)
    {
        return $this->resolveClassAndCreateObject('shipment_item', $shipment);
    }

    /**
     * @inheritdoc
     */
    public function createLineForInvoice(InvoiceInterface $invoice)
    {
        return $this->resolveClassAndCreateObject('invoice_line', $invoice);
    }

    /**
     * @inheritdoc
     */
    public function createPaymentForSale(Model\SaleInterface $sale)
    {
        /** @var \Ekyna\Component\Commerce\Payment\Model\PaymentInterface $payment */
        $payment = $this->resolveClassAndCreateObject('payment', $sale);

        $payment
            ->setCurrency($sale->getCurrency())
            ->setAmount($sale->getRemainingAmount());

        return $payment;
    }

    /**
     * @inheritdoc
     */
    public function createShipmentForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('shipment', $sale);
    }

    /**
     * @inheritdoc
     */
    public function createInvoiceForSale(Model\SaleInterface $sale)
    {
        return $this->resolveClassAndCreateObject('invoice', $sale);
    }

    /**
     * Resolves the class and creates the expected object.
     *
     * @param string $type
     * @param object $subject
     *
     * @return object
     */
    private function resolveClassAndCreateObject($type, $subject)
    {
        foreach ($this->classes[$type] as $source => $target) {
            if ($subject instanceof $source) {
                return new $target;
            }
        }

        throw new InvalidArgumentException('Unsupported object class.');
    }

    /**
     * Returns the default classes.
     *
     * @return array
     */
    private function getDefaultClasses()
    {
        // TODO use constants for keys

        return [
            'address'               => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartAddress::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderAddress::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuoteAddress::class,
            ],
            'attachment'            => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartAttachment::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderAttachment::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuoteAttachment::class,
            ],
            'notification'          => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartNotification::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderNotification::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuoteNotification::class,
            ],
            'item'                  => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartItem::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderItem::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuoteItem::class,
            ],
            'adjustment'            => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartAdjustment::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderAdjustment::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuoteAdjustment::class,
            ],
            'item_adjustment'       => [
                Cart\Model\CartItemInterface::class   => Cart\Entity\CartItemAdjustment::class,
                Order\Model\OrderItemInterface::class => Order\Entity\OrderItemAdjustment::class,
                Quote\Model\QuoteItemInterface::class => Quote\Entity\QuoteItemAdjustment::class,
            ],
            'item_stock_assignment' => [
                Order\Model\OrderItemInterface::class => Order\Entity\OrderItemStockAssignment::class,
            ],
            'payment'               => [
                Cart\Model\CartInterface::class   => Cart\Entity\CartPayment::class,
                Order\Model\OrderInterface::class => Order\Entity\OrderPayment::class,
                Quote\Model\QuoteInterface::class => Quote\Entity\QuotePayment::class,
            ],
            'shipment'              => [
                Order\Model\OrderInterface::class => Order\Entity\OrderShipment::class,
            ],
            'shipment_item'         => [
                Order\Model\OrderShipmentInterface::class => Order\Entity\OrderShipmentItem::class,
            ],
            'invoice'               => [
                Order\Model\OrderInterface::class => Order\Entity\OrderInvoice::class,
            ],
            'invoice_line'          => [
                Order\Model\OrderInvoiceInterface::class => Order\Entity\OrderInvoiceLine::class,
            ],
        ];
    }
}


<?php

namespace PeskyCMF\Scaffold;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use PeskyCMF\Config\CmfConfig;
use PeskyCMF\Http\CmfJsonResponse;
use PeskyCMF\HttpCode;
use PeskyCMF\Scaffold\DataGrid\DataGridConfig;
use PeskyCMF\Scaffold\DataGrid\FilterConfig;
use PeskyCMF\Scaffold\Form\FormConfig;
use PeskyCMF\Scaffold\ItemDetails\ItemDetailsConfig;
use PeskyCMF\Traits\DataValidationHelper;
use PeskyORM\ORM\RecordInterface;
use PeskyORM\ORM\TableInterface;
use PeskyORM\ORM\TempRecord;

abstract class ScaffoldConfig implements ScaffoldConfigInterface {

    use DataValidationHelper;

    /** @var DataGridConfig */
    protected $dataGridConfig = null;
    /** @var FilterConfig */
    protected $dataGridFilterConfig = null;
    /** @var ItemDetailsConfig */
    protected $itemDetailsConfig = null;
    /** @var FormConfig */
    protected $formConfig = null;

    /** @var bool */
    protected $isDetailsViewerAllowed = true;
    /** @var bool */
    protected $isCreateAllowed = true;
    /** @var bool */
    protected $isEditAllowed = true;
    /** @var bool */
    protected $isCloningAllowed = false;
    /** @var bool */
    protected $isDeleteAllowed = true;
    /**
     * Path to localization of views.
     * Usage: see $this->getLocalizationBasePath() method.
     * By default if $viewsBaseTranslationKey is empty - static::getResourceName() will be used
     * @return null|string
     */
    protected $viewsBaseTranslationKey = null;

    /**
     * @var null|ScaffoldLoggerInterface
     */
    protected $logger;

    /**
     * List of record's columns to log on record usage/modification
     * @var null|array
     */
    protected $loggableRecordColumns = null;

    /**
     * List of record's columns that should not be logged on record usage/modification
     * @var null|array
     */
    protected $notLoggableRecordColumns = null;

    /**
     * Should record's file columns be logged on record usage/modification?
     * @var bool
     */
    protected $logFileColumns = true;

    /**
     * List of record's relations and their columns to log together with record's data
     * Note: relation's data will be logged only when it is already loaded (no additional DB queries wil be done)
     * Format: ['Relation1', 'Relation2' => ['column1', 'column2']]; Default: ['*']
     * Use FALSE to disable logging of relations' data
     * @var null|array|false
     */
    protected $loggableRecordRelations = false;

    /**
     * ScaffoldConfig constructor.
     */
    public function __construct() {
        if ($this->viewsBaseTranslationKey === null) {
            $this->viewsBaseTranslationKey = static::getResourceName();
        }
        $this->setLogger(static::getCmfConfig()->getHttpRequestsLogger());
    }

    /**
     * @return CmfConfig
     */
    static public function getCmfConfig() {
        return CmfConfig::getPrimary();
    }

    /**
     * @return \App\Db\Admins\Admin|\Illuminate\Contracts\Auth\Authenticatable|\PeskyCMF\Db\Admins\CmfAdmin|\PeskyCMF\Db\Traits\ResetsPasswordsViaAccessKey
     */
    static public function getUser() {
        return static::getCmfConfig()->getUser();
    }

    /**
     * @return string
     */
    static public function getResourceName() {
        return static::getTable()->getName();
    }

    /**
     * @return array|null
     */
    static public function getMainMenuItem() {
        $resoureName = static::getResourceName();
        $url = routeToCmfItemsTable(static::getResourceName());
        if ($url === null) {
            // access to this menu item was denied
            return null;
        }
        return [
            'label' => cmfTransCustom($resoureName . '.menu_title'),
            'icon' => static::getIconForMenuItem(),
            'url' => $url,
            'counter' => static::getMenuItemCounterName()
        ];
    }

    /**
     * @return null|string
     */
    static protected function getIconForMenuItem() {
        return null;
    }

    /**
     * @return string
     */
    static public function getMenuItemCounterName() {
        return static::getMenuItemCounterValue() ? static::getResourceName() . '_count' : null;
    }

    /**
     * Get value for menu item counter (some html code to display near menu item button: new items count, etc)
     * More info: CmfConfig::menu()
     * You may return an HTML string or \Closure that returns that string.
     * Note that self::getMenuItemCounterName() uses this method to decide if it should return null or counter name.
     * If you want to return HTML string consider overwriting of self::getMenuItemCounterName()
     * @return null|\Closure|string
     */
    static public function getMenuItemCounterValue() {
        return null;
    }

    /**
     * @param array $filters
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemsTable(array $filters = [], $absolute = false) {
        return routeToCmfItemsTable(static::getResourceName(), $filters, $absolute);
    }

    /**
     * @param string $actionId
     * @param array $queryArgs
     * @param bool $absolute
     * @return string
     */
    static public function getUrlCustomAction($actionId, array $queryArgs = [], $absolute = false) {
        return routeToCmfResourceCustomAction(static::getResourceName(), $actionId, $queryArgs, $absolute);
    }

    /**
     * @param string $pageId
     * @param array $queryArgs
     * @param bool $absolute
     * @return string
     */
    static public function getUrlCustomPage($pageId, array $queryArgs = [], $absolute = false) {
        return routeToCmfResourceCustomPage(static::getResourceName(), $pageId, $queryArgs, $absolute);
    }

    /**
     * @param mixed $itemId
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemDetails($itemId, $absolute = false) {
        return routeToCmfItemDetails(static::getResourceName(), $itemId, $absolute);
    }

    /**
     * @param array $data
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemAddForm(array $data = [], $absolute = false) {
        return routeToCmfItemAddForm(static::getResourceName(), $data, $absolute);
    }

    /**
     * @param mixed $itemId
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemEditForm($itemId, $absolute = false) {
        return routeToCmfItemEditForm(static::getResourceName(), $itemId, $absolute);
    }

    /**
     * @param mixed $itemId
     * @param string $actionId
     * @param array $queryArgs
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemCustomAction($itemId, $actionId, array $queryArgs = [], $absolute = false) {
        return routeToCmfItemCustomAction(static::getResourceName(), $itemId, $actionId, $queryArgs, $absolute);
    }

    /**
     * @param mixed $itemId
     * @param string $pageId
     * @param array $queryArgs
     * @param bool $absolute
     * @return string
     */
    static public function getUrlToItemCustomPage($itemId, $pageId, array $queryArgs = [], $absolute = false) {
        return routeToCmfItemCustomPage(static::getResourceName(), $itemId, $pageId, $queryArgs, $absolute);
    }

    /**
     * @return Request
     */
    public function getRequest() {
        return request();
    }

    /**
     * @return array
     * @throws \Exceptions\Data\NotFoundException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws ScaffoldSectionConfigException
     */
    public function getConfigsForTemplatesRendering() {
        $configs = [
            'table' => static::getTable(),
            'scaffoldConfig' => $this
        ];
        $configs['dataGridConfig'] = $this->getDataGridConfig();
        if (!($configs['dataGridConfig'] instanceof DataGridConfig)) {
            throw new ScaffoldSectionConfigException(null, 'createDataGridConfig() should return instance of DataGridConfig class');
        }
        $configs['dataGridFilterConfig'] = $this->getDataGridFilterConfig();
        if (!($configs['dataGridFilterConfig'] instanceof FilterConfig)) {
            throw new ScaffoldSectionConfigException(null, 'createDataGridFilterConfig() should return instance of FilterConfig class');
        }
        $configs['itemDetailsConfig'] = $this->getItemDetailsConfig();
        if (!($configs['itemDetailsConfig'] instanceof ItemDetailsConfig)) {
            throw new ScaffoldSectionConfigException(null, 'createItemDetailsConfig() should return instance of ItemDetailsConfig class');
        }
        $configs['formConfig'] = $this->getFormConfig();
        if (!($configs['formConfig'] instanceof FormConfig)) {
            throw new ScaffoldSectionConfigException(null, 'createFormConfig() should return instance of FormConfig class');
        }
        $configs['dataGridConfig']->beforeRender();
        $configs['dataGridFilterConfig']->beforeRender();
        $configs['itemDetailsConfig']->beforeRender();
        $configs['formConfig']->beforeRender();
        return $configs;
    }

    /**
     * @return DataGridConfig
     */
    protected function createDataGridConfig() {
        return DataGridConfig::create(static::getTable(), $this);
    }

    /**
     * @return FilterConfig
     */
    protected function createDataGridFilterConfig() {
        return FilterConfig::create(static::getTable(), $this);
    }

    /**
     * @return ItemDetailsConfig
     */
    protected function createItemDetailsConfig() {
        return ItemDetailsConfig::create(static::getTable(), $this);
    }

    /**
     * @return FormConfig
     */
    protected function createFormConfig() {
        return FormConfig::create(static::getTable(), $this);
    }

    /**
     * @return DataGridConfig
     * @throws \Exceptions\Data\NotFoundException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function getDataGridConfig() {
        if (empty($this->dataGridConfig)) {
            $this->dataGridConfig = $this->createDataGridConfig();
            $this->dataGridConfig->finish();
        }
        return $this->dataGridConfig;
    }

    /**
     * @return FilterConfig
     */
    public function getDataGridFilterConfig() {
        if (empty($this->dataGridFilterConfig)) {
            $this->dataGridFilterConfig = $this->createDataGridFilterConfig();
            $this->dataGridFilterConfig->finish();
        }
        return $this->dataGridFilterConfig;
    }

    /**
     * @return ItemDetailsConfig
     * @throws \BadMethodCallException
     */
    public function getItemDetailsConfig() {
        if (empty($this->itemDetailsConfig)) {
            $this->itemDetailsConfig = $this->createItemDetailsConfig();
            $this->itemDetailsConfig->finish();
        }
        return $this->itemDetailsConfig;
    }

    /**
     * @return FormConfig
     * @throws \BadMethodCallException
     */
    public function getFormConfig() {
        if (empty($this->formConfig)) {
            $this->formConfig = $this->createFormConfig();
            $this->formConfig->finish();
        }
        return $this->formConfig;
    }

    /**
     * @return bool
     */
    public function isSectionAllowed() {
        return \Gate::allows('resource.view', [static::getResourceName()]);
    }

    /**
     * @return boolean
     */
    public function isCreateAllowed() {
        return (
            $this->isCreateAllowed
            && $this->isSectionAllowed()
            && \Gate::allows('resource.create', [static::getResourceName()])
        );
    }

    /**
     * @return boolean
     */
    public function isEditAllowed() {
        return $this->isEditAllowed && $this->isSectionAllowed();
    }

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

    /**
     * @return boolean
     */
    public function isDetailsViewerAllowed() {
        return $this->isDetailsViewerAllowed && $this->isSectionAllowed();
    }

    /**
     * @return boolean
     */
    public function isDeleteAllowed() {
        return $this->isDeleteAllowed && $this->isSectionAllowed();
    }

    /**
     * Detects if $record deletable or not.
     * Used in child classes to add possibility to disable action depending on record data
     * @param array $record
     * @return bool
     */
    public function isRecordDeleteAllowed(array $record) {
        return $this->isDeleteAllowed() && \Gate::allows('resource.delete', [static::getResourceName(), $record]);
    }

    /**
     * Detects if $record editable or not.
     * Used in child classes to add possibility to disable action depending on record data
     * @param array $record
     * @return bool
     */
    public function isRecordEditAllowed(array $record) {
        return $this->isEditAllowed() && \Gate::allows('resource.update', [static::getResourceName(), $record]);
    }

    /**
     * Detects if $record details can be displayed or not.
     * Used in child classes to add possibility to disable action depending on record data
     * @param array $record
     * @return bool
     */
    public function isRecordDetailsAllowed(array $record) {
        return (
            $this->isDetailsViewerAllowed()
            && \Gate::allows('resource.details', [static::getResourceName(), $record])
        );
    }

    /**
     * @param string $section - main sections are: 'datagrid.column', 'item_details.field', 'form.input'
     * @param AbstractValueViewer $viewer
     * @param string|array $suffix - pass array here to be used as $parameters but $parameters should be empty
     * @param array $parameters
     * @return string
     */
    public function translateForViewer($section, AbstractValueViewer $viewer, $suffix = '', array $parameters = []) {
        if (is_array($suffix) && empty($parameters)) {
            $parameters = $suffix;
            $suffix = '';
        }
        return $this->translate($section, rtrim("{$viewer->getNameForTranslation()}_{$suffix}", '_'), $parameters);
    }

    /**
     * @param string $section - main sections are: 'form.tooltip'
     * @param string|array $suffix - pass array here to be used as $parameters but $parameters should be empty
     * @param array $parameters
     * @return array|string
     */
    public function translate($section, $suffix = '', array $parameters = []) {
        if (is_array($suffix) && empty($parameters)) {
            $parameters = $suffix;
            $suffix = '';
        }
        return cmfTransCustom(rtrim(".{$this->viewsBaseTranslationKey}.{$section}.{$suffix}", '.'), $parameters);
    }

    /**
     * Translate general UI elements (button labels, tooltips, messages, etc..)
     * @param $path
     * @param array $parameters
     * @return mixed
     */
    public function translateGeneral($path, array $parameters = []) {
        $text = $this->translate($path, '', $parameters);
        if (preg_match('%\.' . preg_quote($path, '%') . '$%', $text)) {
            $text = cmfTransGeneral($path, $parameters);
        }
        return $text;
    }

    public function renderTemplates() {
        if (!$this->isSectionAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.access_denied_to_scaffold'));
        }
        return static::getCmfConfig()->getUiModule()->renderScaffoldTemplates($this);
    }

    /**
     * @return array
     */
    public function renderTemplatesAndSplit() {
        $blocks = [
            'datagrid' => false,
            'itemForm' => false,
            'bulkEditForm' => false,
            'itemDetails' => false,
            'itemFormDefaults' => false
        ];
        if (!$this instanceof KeyValueTableScaffoldConfig && ($this->isCreateAllowed() || $this->isEditAllowed())) {
            /** @var JsonResponse $response */
            $response = $this->getDefaultValuesForFormInputs();
            $blocks['itemFormDefaults'] = $response->getData(true);
        }
        $html = $this->renderTemplates();
        foreach ($blocks as $block => &$template) {
            if (preg_match("%<!--\s*{$block}\s*start\s*-->(?:\s*\n*)*(.*?)<!--\s*{$block}\s*end\s*-->%is", $html, $matches)) {
                $template = trim(preg_replace(
                    ['%^\s*<(div|script)[^>]+id="(data-grid-tpl|item-form-tpl|item-details-tpl|bulk-edit-form-tpl)"[^>]*>\s*(.*)\s*</\1>\s*$%is', '%^\s+%im'],
                    ['$3', ' '],
                    $matches[1]
                ));
            }
        }
        return $blocks;
    }

    public function getHtmlOptionsForFormInputs() {
        if (!$this->isSectionAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.access_denied_to_scaffold'));
        }
        if (!$this->isEditAllowed() && !$this->isCreateAllowed()) {
            return $this->makeAccessDeniedReponse($this->getFormConfig()->translateGeneral('message.edit.forbidden'));
        }
        $formConfig = $this->getFormConfig();
        $columnsOptions = $formConfig->loadOptions($this->getRequest()->query('id'));
        foreach ($columnsOptions as $columnName => $options) {
            if (is_array($options)) {
                $columnsOptions[$columnName] = $this->renderOptionsForSelectInput(
                    $options,
                    $formConfig->getValueViewer($columnName)->getEmptyOptionLabel()
                );
            } else if (!is_string($options)) {
                unset($columnsOptions[$columnName]);
            }
        }
        return cmfJsonResponse()->setData($columnsOptions);
    }

    public function getJsonOptionsForFormInput($inputName) {
        if (!$this->isSectionAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.access_denied_to_scaffold'));
        }
        if (!$this->isEditAllowed() && !$this->isCreateAllowed()) {
            return $this->makeAccessDeniedReponse($this->getFormConfig()->translateGeneral('message.edit.forbidden'));
        }
        $formConfig = $this->getFormConfig();
        $options = $formConfig->loadOptionsForInput(
            $inputName,
            $this->getRequest()->query('id'),
            $this->getRequest()->query('keywords')
        );
        return cmfJsonResponse()->setData($options);
    }

    /**
     * @param array $options
     * @param bool|string $addEmptyOption - false: do not add default empty option | string: add default empty option
     * @return string
     */
    protected function renderOptionsForSelectInput(array $options, $addEmptyOption = false) {
        $ret = '';
        $hasEmptyOption = array_key_exists('', $options);
        if ($addEmptyOption !== false || $hasEmptyOption) {
            if ($hasEmptyOption) {
                $label = $options[''];
                unset($options['']);
            } else {
                $label = $addEmptyOption === true ? '' : $addEmptyOption;
            }
            $ret .= '<option value="">' . $label . '</option>';
        }
        foreach ($options as $value => $label) {
            if (!is_array($label)) {
                $ret .= '<option value="' . htmlentities($value) . '">' . $label . '</option>';
            } else {
                $ret .= '<optgroup label="' . htmlentities($value) . '">' . $this->renderOptionsForSelectInput($label) . '</optgroup>';
            }
        }
        return $ret;
    }

    /**
     * @param TableInterface $table
     * @param null|string $message
     * @return $this
     */
    protected function makeRecordNotFoundResponse(TableInterface $table, $message = null) {
        if (empty($message)) {
            $message = $this->translateGeneral('message.resource_item_not_found');
        }
        return cmfJsonResponseForHttp404(
            routeToCmfItemsTable(static::getResourceName()),
            $message
        );
    }

    /**
     * @param string $message
     * @return CmfJsonResponse
     */
    protected function makeAccessDeniedReponse($message) {
        return cmfJsonResponse(HttpCode::FORBIDDEN)
            ->setMessage($message)
            ->goBack(routeToCmfItemsTable(static::getResourceName()));
    }

    /**
     * @param ScaffoldLoggerInterface $logger
     * @return $this
     */
    public function setLogger(ScaffoldLoggerInterface $logger = null) {
        $this->logger = $logger;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasLogger() {
        return $this->logger !== null;
    }

    /**
     * @return null|ScaffoldLoggerInterface
     */
    public function getLogger() {
        return $this->logger;
    }

    /**
     * @param RecordInterface $record
     * @param null|string $tableName
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function logDbRecordBeforeChange(RecordInterface $record, $tableName = null) {
        if ($this->hasLogger()) {
            $this->getLogger()->logDbRecordBeforeChange(
                $record,
                $tableName,
                $this->getLoggableRecordColumns($record),
                $this->getLoggableRecordRelations($record)
            );
        }
        return $this;
    }

    /**
     * @param RecordInterface $record
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function logDbRecordAfterChange(RecordInterface $record) {
        if ($this->hasLogger()) {
            $this->getLogger()->logDbRecordAfterChange(
                $record,
                $this->getLoggableRecordColumns($record),
                $this->getLoggableRecordRelations($record)
            );
        }
        return $this;
    }

    /**
     * @param RecordInterface $record
     * @param null|string $tableName
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    public function logDbRecordLoad(RecordInterface $record, $tableName = null) {
        if ($this->hasLogger()) {
            $this->getLogger()->logDbRecordUsage($record, $tableName);
        }
        return $this;
    }

    /**
     * @param RecordInterface $record
     * @return array
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    protected function getLoggableRecordColumns(RecordInterface $record) {
        if (is_array($this->loggableRecordColumns)) {
            $fields = $this->loggableRecordColumns;
        } else if ($record instanceof TempRecord) {
            return null;
        } else {
            $fields = array_keys($record::getTable()->getTableStructure()->getColumns());
        }
        if (is_array($this->notLoggableRecordColumns)) {
            $fields = array_diff($fields, $this->notLoggableRecordColumns);
        }
        if (!$this->logFileColumns) {
            $fields = array_diff($fields, array_keys($record::getTable()->getTableStructure()->getFileColumns()));
        }
        $fields[] = $record::getTable()->getTableStructure()->getPkColumnName();
        return array_unique($fields);
    }

    /**
     * @param RecordInterface $record
     * @return array
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    protected function getLoggableRecordRelations(RecordInterface $record) {
        if ($this->loggableRecordRelations === false || $record instanceof TempRecord) {
            return [];
        } else if (is_array($this->loggableRecordRelations)) {
            return $this->loggableRecordRelations;
        } else {
            return array_keys($record::getTable()->getTableStructure()->getRelations());
        }
    }

    public function getCustomData($dataId) {
        return cmfJsonResponse(HttpCode::NOT_FOUND);
    }

    public function getCustomPage($pageName) {
        return view('cmf::ui.default_page_header', [
            'header' => 'Handler for route [' . request()->getPathInfo() . '] is not defined in' . static::class . '->getCustomPage()',
        ]);
    }

    /**
     * Call scaffold's method called $actionName without arguments
     * @param string $actionName
     * @return $this
     */
    public function performAction($actionName) {
        if (method_exists($this, $actionName)) {
            return $this->$actionName();
        } else {
            return cmfJsonResponse(HttpCode::NOT_FOUND)
                ->setMessage('Method [' . static::class . '->' . $actionName . '] is not defined');
        }
    }

    public function getCustomPageForRecord($itemId, $pageName) {
        return view('cmf::ui.default_page_header', [
            'header' => 'Handler for route [' . request()->getPathInfo() . '] is not defined in ' . static::class . '->getCustomPageForRecord()',
        ]);
    }

    /**
     * Call scaffold's method called $actionName with $itemId argument
     * @param string $itemId
     * @param string $actionName
     * @return $this
     */
    public function performActionForRecord($itemId, $actionName) {
        if (method_exists($this, $actionName)) {
            return $this->$actionName($itemId);
        } else {
            return cmfJsonResponse(HttpCode::NOT_FOUND)
                ->setMessage('Method [' . static::class . '->' . $actionName . '] is not defined');
        }
    }

}


<?php

namespace Ekyna\Component\Commerce\Invoice\Resolver;

use Ekyna\Component\Commerce\Common\Resolver\StateResolverInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Invoice\Calculator\InvoiceCalculatorInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceStates;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceSubjectInterface;
use Ekyna\Component\Commerce\Payment\Model\PaymentStates;
use Ekyna\Component\Commerce\Payment\Model\PaymentSubjectInterface;

/**
 * Class InvoiceSubjectStateResolver
 * @package Ekyna\Component\Commerce\Invoice\Resolver
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class InvoiceSubjectStateResolver implements StateResolverInterface
{
    /**
     * @var InvoiceCalculatorInterface
     */
    protected $calculator;

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

    /**
     * @inheritdoc
     */
    public function resolve($subject)
    {
        if (!$subject instanceof InvoiceSubjectInterface) {
            throw new InvalidArgumentException("Expected instance of " . InvoiceSubjectInterface::class);
        }

        $quantities = $this->calculator->buildInvoiceQuantityMap($subject);
        if (0 === $itemsCount = count($quantities)) {
            return $this->setState($subject, InvoiceStates::STATE_NEW);
        }

        $partialCount = $invoicedCount = $creditedCount = 0;

        foreach ($quantities as $q) {
            // TODO Use packaging format
            // If invoiced greater than zero
            if (0 < $q['invoiced']) {
                // If total equals invoiced, item is fully invoiced
                //if ($q['total'] == $q['invoiced']) {
                if (0 === bccomp($q['total'], $q['invoiced'], 3)) {
                    $invoicedCount++;

                    // If invoiced equals credited, item is fully credited
                    if (0 === bccomp($q['invoiced'], $q['credited'], 3)) {
                        $creditedCount++;
                    }

                    continue;
                }

                // Item is partially invoiced
                $partialCount++;
            }
        }

        // TODO Assert sale's shipment and discounts are invoiced

        // If all fully credited
        if ($creditedCount == $itemsCount) {
            return $this->setState($subject, InvoiceStates::STATE_CREDITED);
        }

        // If all fully invoiced
        if ($invoicedCount == $itemsCount) {
            return $this->setState($subject, InvoiceStates::STATE_COMPLETED);
        }

        // If some partially invoiced
        if (0 < $partialCount || 0 < $invoicedCount) {
            return $this->setState($subject, InvoiceStates::STATE_PARTIAL);
        }

        // CANCELED If subject has payment(s) and has canceled state
        if ($subject instanceof PaymentSubjectInterface){
            if (in_array($subject->getPaymentState(), PaymentStates::getCanceledStates(), true)) {
                return $this->setState($subject, InvoiceStates::STATE_CANCELED);
            }
        }

        // NEW by default
        return $this->setState($subject, InvoiceStates::STATE_NEW);
    }

    /**
     * Sets the shipment state.
     *
     * @param InvoiceSubjectInterface $subject
     * @param string                   $state
     *
     * @return bool Whether the shipment state has been updated.
     */
    protected function setState(InvoiceSubjectInterface $subject, $state)
    {
        if ($state !== $subject->getInvoiceState()) {
            $subject->setInvoiceState($state);

            return true;
        }

        return false;
    }
}


<?php

namespace Ekyna\Component\Commerce\Shipment\Calculator;

use Ekyna\Component\Commerce\Common\Model as Common;
use Ekyna\Component\Commerce\Invoice\Calculator\InvoiceCalculatorInterface;
use Ekyna\Component\Commerce\Shipment\Model as Shipment;
use Ekyna\Component\Commerce\Stock\Model as Stock;
use Ekyna\Component\Commerce\Subject\SubjectHelperInterface;

/**
 * Class ShipmentCalculator
 * @package Ekyna\Component\Commerce\Shipment\Calculator
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ShipmentCalculator implements ShipmentCalculatorInterface
{
    /**
     * @var SubjectHelperInterface
     */
    private $subjectHelper;

    /**
     * @var InvoiceCalculatorInterface
     */
    private $invoiceCalculator;

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

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

    /**
     * @inheritdoc
     */
    public function isShipped(Common\SaleItemInterface $saleItem)
    {
        // If compound with only public children
        if ($saleItem->isCompound() && !$saleItem->hasPrivateChildren()) {
            // Shipped if any of it's children is
            foreach ($saleItem->getChildren() as $child) {
                if ($this->isShipped($child)) {
                    return true;
                }
            }

            return false;
        }

        $sale = $saleItem->getSale();
        if (!$sale instanceof Shipment\ShipmentSubjectInterface) {
            return false;
        }

        foreach ($sale->getShipments() as $shipment) {
            foreach ($shipment->getItems() as $line) {
                if ($line->getSaleItem() === $saleItem) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @inheritdoc
     */
    public function calculateAvailableQuantity(
        Common\SaleItemInterface $saleItem,
        Shipment\ShipmentInterface $ignore = null
    ) {
        /** @var Common\SaleItemInterface $saleItem */
        if (!$this->hasStockableSubject($saleItem)) {
            return INF;
        }

        // TODO Packaging format
        /** @var Stock\StockAssignmentsInterface $saleItem */
        $quantity = 0;
        foreach ($saleItem->getStockAssignments() as $assignment) {
            $quantity += $assignment->getShippableQuantity();
        }

        if (
            null !== $ignore && null !== $ignore->getId() && !$ignore->isReturn() &&
            Shipment\ShipmentStates::isStockableState($ignore->getState())
        ) {
            foreach ($ignore->getItems() as $item) {
                if ($item->getSaleItem() === $saleItem) {
                    $quantity += $item->getQuantity();

                    break;
                }
            }
        }

        return max($quantity, 0);
    }

    /**
     * @inheritdoc
     *
     * @todo Add bool $strict parameter : really shipped and not created/prepared
     */
    public function calculateShippableQuantity(
        Common\SaleItemInterface $saleItem,
        Shipment\ShipmentInterface $ignore = null
    ) {
        // TODO Return zero if not shippable (?)

        // Quantity = Sold - Shipped - Returned (ignoring current)

        // TODO Packaging format
        $quantity = $saleItem->getTotalQuantity();
        $quantity -= $this->invoiceCalculator->calculateCreditedQuantity($saleItem);
        $quantity -= $this->calculateShippedQuantity($saleItem, $ignore);
        $quantity += $this->calculateReturnedQuantity($saleItem);

        return max($quantity, 0);
    }

    /**
     * @inheritdoc
     */
    public function calculateReturnableQuantity(
        Common\SaleItemInterface $saleItem,
        Shipment\ShipmentInterface $ignore = null
    ) {
        // Quantity = Shipped - Returned (ignoring current)

        // TODO Packaging format
        $quantity = $this->calculateShippedQuantity($saleItem)
            - $this->calculateReturnedQuantity($saleItem, $ignore);

        return max($quantity, 0);
    }

    /**
     * @inheritdoc
     */
    public function calculateShippedQuantity(
        Common\SaleItemInterface $saleItem,
        Shipment\ShipmentInterface $ignore = null
    ) {
        $sale = $saleItem->getSale();

        if (!$sale instanceof Shipment\ShipmentSubjectInterface) {
            return 0;
        }

        // TODO Packaging format
        $quantity = 0;

        foreach ($sale->getShipments() as $shipment) {
            if (null !== $ignore && $shipment === $ignore) {
                continue;
            }

            if ($shipment->isReturn() || $shipment->getState() !== Shipment\ShipmentStates::STATE_SHIPPED) {
                continue;
            }

            foreach ($shipment->getItems() as $line) {
                if ($line->getSaleItem() === $saleItem) {
                    $quantity += $line->getQuantity();
                }
            }
        }

        return $quantity;
    }

    /**
     * @inheritdoc
     */
    public function calculateReturnedQuantity(
        Common\SaleItemInterface $saleItem,
        Shipment\ShipmentInterface $ignore = null
    ) {
        $sale = $saleItem->getSale();

        if (!$sale instanceof Shipment\ShipmentSubjectInterface) {
            return 0;
        }

        // TODO Packaging format
        $quantity = 0;

        foreach ($sale->getShipments() as $shipment) {
            if (null !== $ignore && $shipment === $ignore) {
                continue;
            }

            if (!$shipment->isReturn() || $shipment->getState() !== Shipment\ShipmentStates::STATE_RETURNED) {
                continue;
            }

            foreach ($shipment->getItems() as $shipmentItem) {
                if ($shipmentItem->getSaleItem() === $saleItem) {
                    $quantity += $shipmentItem->getQuantity();
                }
            }
        }

        return $quantity;
    }

    /**
     * @inheritdoc
     */
    public function buildShipmentQuantityMap(Shipment\ShipmentSubjectInterface $subject)
    {
        $quantities = [];

        if ($subject instanceof Common\SaleInterface) {
            foreach ($subject->getItems() as $item) {
                $this->buildSaleItemQuantities($item, $quantities);
            }
        }

        return $quantities;
    }

    /**
     * @inheritdoc
     */
    public function buildRemainingList(Shipment\ShipmentInterface $shipment)
    {
        $sale = $shipment->getSale();

        $id = $shipment->getId();
        $date = $shipment->getShippedAt() ?? $shipment->getCreatedAt();

        $shipments = $sale
            ->getShipments()
            ->filter(function (Shipment\ShipmentInterface $s) use ($id, $date) {
                if ($s->getId() == $id) {
                    return false;
                }

                if (!Shipment\ShipmentStates::isStockableState($s->getState())) {
                    return false;
                }

                if ($s->getShippedAt() > $date) {
                    return false;
                }

                return true;
            })
            ->toArray();

        $shipments[] = $shipment;
        $list = new Shipment\RemainingList();

        foreach ($sale->getItems() as $item) {
            $this->buildSaleItemRemaining($item, $list, $shipments);
        }

        $esd = null;
        foreach ($list->getEntries() as $entry) {
            /** @var Common\SaleItemInterface $saleItem */
            $saleItem = $entry->getSaleItem();

            if (!$saleItem instanceof Stock\StockAssignmentsInterface) {
                continue;
            }

            /** @var Stock\StockSubjectInterface */
            if (null === $subject = $this->subjectHelper->resolve($saleItem, false)) {
                continue;
            }

            if (!$subject instanceof Stock\StockSubjectInterface) {
                continue;
            }

            $quantity = $entry->getQuantity();
            $eda = null;
            foreach ($saleItem->getStockAssignments() as $assignment) {
                $quantity -= $assignment->getShippableQuantity();

                // Abort if item is shippable
                if (0 >= $quantity) {
                    break;
                }

                // Next assignment if we don't have an EDA
                if (null === $date = $assignment->getStockUnit()->getEstimatedDateOfArrival()) {
                    continue;
                }

                if (is_null($eda) || $eda < $date) {
                    $eda = $date;
                }
            }

            if (is_null($eda)) {
                continue;
            }

            if (is_null($esd) || $esd < $eda) {
                $esd = $eda;
            }
        }

        if ($esd > new \DateTime()) {
            $list->setEstimatedShippingDate($esd);
        }

        return $list;
    }

    /**
     * Builds the sale item quantities recursively.
     *
     * @param Common\SaleItemInterface $item
     * @param array                    $quantities
     */
    private function buildSaleItemQuantities(Common\SaleItemInterface $item, array &$quantities)
    {
        // Skip compound with only public children
        if (!($item->isCompound() && !$item->hasPrivateChildren())) {
            $quantities[$item->getId()] = [
                'total'    => $item->getTotalQuantity(),
                'credited' => $this->invoiceCalculator->calculateCreditedQuantity($item),
                'shipped'  => $this->calculateShippedQuantity($item),
                'returned' => $this->calculateReturnedQuantity($item),
            ];
        }

        if ($item->hasChildren()) {
            foreach ($item->getChildren() as $child) {
                $this->buildSaleItemQuantities($child, $quantities);
            }
        }
    }

    /**
     * Calculate the sale item remaining quantity.
     *
     * @param Common\SaleItemInterface     $saleItem
     * @param Shipment\RemainingList       $list
     * @param Shipment\ShipmentInterface[] $shipments
     */
    private function buildSaleItemRemaining(
        Common\SaleItemInterface $saleItem,
        Shipment\RemainingList $list,
        array $shipments
    ) {

        // Not for compound item with only public children
        if (!($saleItem->isCompound() && !$saleItem->hasPrivateChildren())) {
            $quantity = $saleItem->getTotalQuantity();

            foreach ($shipments as $shipment) {
                foreach ($shipment->getItems() as $item) {
                    if ($item->getSaleItem() === $saleItem) {
                        $quantity += $shipment->isReturn() ? $item->getQuantity() : -$item->getQuantity();

                        continue 2;
                    }
                }
            }

            if (0 < $quantity) {
                $entry = new Shipment\RemainingEntry();
                $entry
                    ->setSaleItem($saleItem)
                    ->setQuantity($quantity);

                $list->addEntry($entry);
            }
        }

        foreach ($saleItem->getChildren() as $child) {
            $this->buildSaleItemRemaining($child, $list, $shipments);
        }
    }

    /**
     * Returns whether or not the sale item has a stockable subject.
     *
     * @param Common\SaleItemInterface $saleItem
     *
     * @return bool
     */
    private function hasStockableSubject(Common\SaleItemInterface $saleItem)
    {
        if (!$saleItem instanceof Stock\StockAssignmentsInterface) {
            return false;
        }

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

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

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

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

        return true;
    }
}


<?php

namespace mii\cache;

class Apcu extends Cache
{

    /**
     * Check for existence of the APC extension This method cannot be invoked externally. The driver must
     * be instantiated using the `Cache::instance()` method.
     *
     * @param  array $config configuration
     * @throws CacheException
     */
    public function init(array $config = []): void {

        if (!extension_loaded('apcu')) {
            throw new CacheException('PHP APC extension is not available.');
        }

        parent::init($config);
    }

    /**
     * Retrieve a cached value entry by id.
     *
     *     // Retrieve cache entry from apc group
     *     $data = Cache::instance('apc')->get('foo');
     *
     *     // Retrieve cache entry from apc group and return 'bar' if miss
     *     $data = Cache::instance('apc')->get('foo', 'bar');
     *
     * @param   string $id id of cache to entry
     * @param   string $default default value to return if cache miss
     * @return  mixed
     * @throws  CacheException
     */
    public function get($id, $default = NULL) {
        $data = \apcu_fetch($this->_sanitize_id($id), $success);

        return $success ? $data : $default;
    }

    /**
     * Set a value to cache with id and lifetime
     *
     *     $data = 'bar';
     *
     *     // Set 'bar' to 'foo' in apc group, using default expiry
     *     Cache::instance('apc')->set('foo', $data);
     *
     *     // Set 'bar' to 'foo' in apc group for 30 seconds
     *     Cache::instance('apc')->set('foo', $data, 30);
     *
     * @param   string $id id of cache entry
     * @param   string $data data to set to cache
     * @param   integer $lifetime lifetime in seconds
     * @return  boolean
     */
    public function set($id, $data, $lifetime = NULL) {
        if ($lifetime === NULL) {
            $lifetime = $this->default_expire;
        }

        return \apcu_store($this->_sanitize_id($id), $data, $lifetime);
    }

    /**
     * Delete a cache entry based on id
     *
     *     // Delete 'foo' entry from the apc group
     *     Cache::instance('apc')->delete('foo');
     *
     * @param   string $id id to remove from cache
     * @return  boolean
     */
    public function delete($id) {
        return \apcu_delete($this->_sanitize_id($id));
    }

    /**
     * Delete all cache entries.
     *
     * Beware of using this method when
     * using shared memory cache systems, as it will wipe every
     * entry within the system for all clients.
     *
     *     // Delete all cache entries in the apc group
     *     Cache::instance('apc')->delete_all();
     *
     */
    public function delete_all(): bool {
        return \apcu_clear_cache();
    }

    /**
     * Increments a given value by the step value supplied.
     * Useful for shared counters and other persistent integer based
     * tracking.
     *
     * @param   string $id of cache entry to increment
     * @param   int $step value to increment by
     * @return  integer
     * @return  boolean
     */
    public function increment($id, $step = 1) {
        return \apcu_inc($id, $step);
    }

    /**
     * Decrements a given value by the step value supplied.
     * Useful for shared counters and other persistent integer based
     * tracking.
     *
     * @param   string $id of cache entry to decrement
     * @param   int $step value to decrement by
     * @return  integer
     * @return  boolean
     */
    public function decrement($id, $step = 1) {
        return \apcu_dec($id, $step);
    }

    protected function _sanitize_id($id): string {
        return $this->prefix . $id;
    }
}


<?php

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

use Ekyna\Component\Commerce\Common\Util\FormatterAwareTrait;
use Ekyna\Component\Commerce\Stock\Model\StockUnitInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

/**
 * Class StockUnitNormalizer
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Serializer\Normalizer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StockUnitNormalizer extends AbstractResourceNormalizer
{
    use FormatterAwareTrait;

    /**
     * @inheritdoc
     *
     * @param StockUnitInterface $unit
     */
    public function normalize($unit, $format = null, array $context = [])
    {
        $data = [];

        if ($this->contextHasGroup(['StockView', 'StockAssignment'], $context)) {
            $formatter = $this->getFormatter();

            if (null !== $eda = $unit->getEstimatedDateOfArrival()) {
                $eda = $formatter->date($eda);
            }

            $adjustments = [];
            $assignments = [];

            if ($this->contextHasGroup('StockView', $context)) {
                foreach ($unit->getStockAdjustments() as $adjustment) {
                    $adjustments[] = $this->normalizeObject($adjustment, $format, $context);
                }
                foreach ($unit->getStockAssignments() as $assignment) {
                    $assignments[] = $this->normalizeObject($assignment, $format, $context);
                }
            }

            $data = array_replace($data, [
                'geocodes'    => implode(',', $unit->getGeocodes()),
                'ordered'     => $formatter->number($unit->getOrderedQuantity()),
                'received'    => $formatter->number($unit->getReceivedQuantity()),
                'adjusted'    => $formatter->number($unit->getAdjustedQuantity()),
                'sold'        => $formatter->number($unit->getSoldQuantity()),
                'shipped'     => $formatter->number($unit->getShippedQuantity()),
                'eda'         => $eda,
                'net_price'   => $formatter->currency($unit->getNetPrice(), $unit->getCurrency()),
                'adjustments' => $adjustments,
                'assignments' => $assignments,
            ]);
        }

        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 StockUnitInterface;
    }

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


<?php

namespace Ekyna\Component\Commerce\Common\Preparer;

use Ekyna\Component\Commerce\Common\Factory\SaleFactoryInterface;
use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Exception\IllegalOperationException;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Order\Event\OrderEvents;
use Ekyna\Component\Commerce\Order\Model\OrderInterface;
use Ekyna\Component\Commerce\Shipment\Builder\ShipmentBuilderInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentStates;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentSubjectInterface;
use Ekyna\Component\Commerce\Stock\Prioritizer\StockPrioritizerInterface;
use Ekyna\Component\Resource\Dispatcher\ResourceEventDispatcherInterface;

/**
 * Class SalePreparer
 * @package Ekyna\Component\Commerce\Common\Preparer
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SalePreparer implements SalePreparerInterface
{
    /**
     * @var ResourceEventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @var StockPrioritizerInterface
     */
    private $stockPrioritizer;

    /**
     * @var ShipmentBuilderInterface
     */
    private $shipmentBuilder;

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

    /**
     * Constructor.
     *
     * @param ResourceEventDispatcherInterface $eventDispatcher
     * @param StockPrioritizerInterface        $stockPrioritizer
     * @param ShipmentBuilderInterface         $shipmentBuilder
     * @param SaleFactoryInterface             $saleFactory
     */
    public function __construct(
        ResourceEventDispatcherInterface $eventDispatcher,
        StockPrioritizerInterface $stockPrioritizer,
        ShipmentBuilderInterface $shipmentBuilder,
        SaleFactoryInterface $saleFactory
    ) {
        $this->eventDispatcher = $eventDispatcher;
        $this->stockPrioritizer = $stockPrioritizer;
        $this->shipmentBuilder = $shipmentBuilder;
        $this->saleFactory = $saleFactory;
    }

    /**
     * @inheritdoc
     */
    public function prepare(SaleInterface $sale)
    {
        if (!$sale instanceof ShipmentSubjectInterface) {
            return null;
        }

        if (!$this->dispatchPrepareEvent($sale)) {
            return null;
        }

        if (!ShipmentStates::isPreparableState($sale->getShipmentState())) {
            return null;
        }

        if ($this->stockPrioritizer->canPrioritizeSale($sale)) {
            $this->stockPrioritizer->prioritizeSale($sale);
        }

        $shipment = $this->saleFactory->createShipmentForSale($sale);

        $sale->addShipment($shipment);

        $this->shipmentBuilder->build($shipment);

        $this->purge($shipment);

        if ($shipment->isEmpty()) {
            $sale->removeShipment($shipment);

            return null;
        }

        $shipment->setState(ShipmentStates::STATE_PREPARATION);

        return $shipment;
    }

    /**
     * Purges the shipment by removing items which are not available.
     *
     * @param ShipmentInterface $shipment
     */
    protected function purge(ShipmentInterface $shipment)
    {
        foreach ($shipment->getItems() as $item) {
            if (0 == $item->getAvailable()) {
                $shipment->removeItem($item);
            }
        }
    }

    /**
     * @inheritdoc
     */
    public function abort(SaleInterface $sale)
    {
        if (!$sale instanceof ShipmentSubjectInterface) {
            return null;
        }

        foreach ($sale->getShipments() as $shipment) {
            if ($shipment->getState() === ShipmentStates::STATE_PREPARATION) {
                return $shipment;
            }
        }

        return null;
    }

    /**
     * Dispatches the sale prepare event.
     *
     * @param SaleInterface $sale
     *
     * @return bool
     */
    protected function dispatchPrepareEvent(SaleInterface $sale)
    {
        if (!$sale instanceof OrderInterface) {
            throw new InvalidArgumentException("Expected instance of " . OrderInterface::class);
        }

        $event = $this->eventDispatcher->createResourceEvent($sale);

        try {
            $this->eventDispatcher->dispatch(OrderEvents::PREPARE, $event);
        } catch (IllegalOperationException $e) {
            return false;
        }

        return true;
    }
}


<?php

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

use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Shipment\Gateway;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class SaleValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class SaleValidator extends ConstraintValidator
{
    /**
     * @var Gateway\RegistryInterface
     */
    private $gatewayRegistry;

    /**
     * Constructor.
     *
     * @param Gateway\RegistryInterface $gatewayRegistry
     */
    public function __construct(Gateway\RegistryInterface $gatewayRegistry)
    {
        $this->gatewayRegistry = $gatewayRegistry;
    }

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

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

        $this->validateIdentity($sale, $constraint);
        $this->validateDeliveryAddress($sale, $constraint);
        $this->validateShipmentMethodRequirements($sale, $constraint);
        $this->validatePaymentTermAndOutstandingLimit($sale, $constraint);

        if (0 < $sale->getDepositTotal() && $sale->getDepositTotal() >= $sale->getGrandTotal()) {
            $this->context
                ->buildViolation($constraint->deposit_greater_than_grand_total)
                ->atPath('depositTotal')
                ->addViolation();
        }
    }

    /**
     * Validates the shipment method requirements.
     *
     * @param SaleInterface $sale
     * @param Constraint    $constraint
     */
    protected function validateShipmentMethodRequirements(SaleInterface $sale, Constraint $constraint)
    {
        if (null === $method = $sale->getShipmentMethod()) {
            return;
        }

        if ($sale->isSameAddress()) {
            $address = $sale->getInvoiceAddress();
            $path = 'invoiceAddress';
        } else {
            $address = $sale->getDeliveryAddress();
            $path = 'deliveryAddress';
        }

        if (null === $address) {
            return;
        }

        $gateway = $this->gatewayRegistry->getGateway($method->getGatewayName());

        if ($gateway->requires(Gateway\GatewayInterface::REQUIREMENT_MOBILE)) {
            if (is_null($address->getMobile())) {
                $this->context
                    ->buildViolation($constraint->shipment_method_require_mobile)
                    ->atPath($path . '.mobile')
                    ->addViolation();
            }
        }
    }

    /**
     * Validates the delivery address.
     *
     * @param SaleInterface $sale
     * @param Constraint    $constraint
     */
    protected function validateDeliveryAddress(SaleInterface $sale, Constraint $constraint)
    {
        /** @var Sale $constraint */
        if (!$sale->isSameAddress() && null === $sale->getDeliveryAddress()) {
            $this->context
                ->buildViolation($constraint->delivery_address_is_required)
                ->atPath('deliveryAddress')
                ->addViolation();

        } elseif ($sale->isSameAddress() && null !== $sale->getDeliveryAddress()) {
            $this->context
                ->buildViolation($constraint->delivery_address_should_be_null)
                ->atPath('deliveryAddress')
                ->addViolation();
        }
    }

    /**
     * Validates the sale identity.
     *
     * @param SaleInterface $sale
     * @param Constraint    $constraint
     */
    protected function validateIdentity(SaleInterface $sale, Constraint $constraint)
    {
        /** @var Sale $constraint */
        if (null === $sale->getCustomer()) {
            if (null === $sale->getCustomerGroup()) {
                $this->context
                    ->buildViolation($constraint->customer_group_is_required_if_no_customer)
                    ->atPath('customerGroup')
                    ->addViolation();
            }
            if (0 == strlen($sale->getEmail())) {
                $this->context
                    ->buildViolation($constraint->email_is_required_if_no_customer)
                    ->atPath('email')
                    ->addViolation();
            }

            IdentityValidator::validateIdentity($this->context, $sale);
        }
    }

    /**
     * Validates the sale payment term and outstanding limit.
     *
     * @param SaleInterface $sale
     * @param Constraint    $constraint
     */
    protected function validatePaymentTermAndOutstandingLimit(SaleInterface $sale, Constraint $constraint)
    {
        if (0 >= $sale->getOutstandingLimit()) {
            return;
        }

        if (null === $term = $sale->getPaymentTerm()) {
            if (null !== $customer = $sale->getCustomer()) {
                // From parent if available
                if ($customer->hasParent()) {
                    $term = $customer->getParent()->getPaymentTerm();
                } else {
                    $term = $customer->getPaymentTerm();
                }
            }
        }

        if (null === $term) {
            /** @var Sale $constraint */
            $this->context
                ->buildViolation($constraint->outstanding_limit_require_term)
                ->atPath('outstandingLimit')
                ->addViolation();
        }
    }
}


<?php

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

use Ekyna\Component\Commerce\Shipment\Model\ShipmentItemInterface;
use Ekyna\Component\Resource\Serializer\AbstractResourceNormalizer;

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

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

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

        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 ShipmentItemInterface;
    }

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


<?php

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

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\Exception\RuntimeException;
use Payum\Core\GatewayAwareInterface;
use Payum\Core\GatewayAwareTrait;
use Payum\Core\Request\Convert;
use Payum\Core\Request\GetCurrency;

/**
 * Class ConvertAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\Sips\Action
 * @author  Étienne Dauvergne <contact@ekyna.com>
 */
class ConvertAction implements ActionInterface, GatewayAwareInterface
{
    use GatewayAwareTrait;

    /**
     * {@inheritDoc}
     *
     * @param Convert $request
     */
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);

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

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

        //$model['DESCRIPTION'] = $payment->getDescription();
        if (false == $model['amount']) {
            $this->gateway->execute($currency = new GetCurrency($payment->getCurrency()->getCode()));
            if (2 < $currency->exp) {
                throw new RuntimeException('Unexpected currency exp.');
            }
            $model['currency_code'] = $currency->numeric;
            // Amount in cents
            $model['amount'] = abs($payment->getAmount() * pow(10, $currency->exp));
        }

        $sale = $payment->getSale();

        if (false == $model['order_id']) {
            // TODO sale number + Payment number
            $model['order_id'] = $sale->getNumber();
        }

        $sale = $payment->getSale();
        if (false == $model['customer_id'] && null !== $customer = $sale->getCustomer()) {
            $model['customer_id'] = $customer->getId();
        }
        if (false == $model['customer_email']) {
            $model['customer_email'] = $sale->getEmail();
        }

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

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


<?php

namespace Ekyna\Component\Commerce\Shipment\EventListener;

use Behat\Transliterator\Transliterator;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentMethodInterface;
use Ekyna\Component\Resource\Event\ResourceEventInterface;
use Ekyna\Component\Resource\Persistence\PersistenceHelperInterface;

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

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

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

        if (0 == strlen($method->getGatewayName())) {
            $method->setGatewayName(sprintf(
                '%s-%s',
                Transliterator::transliterate($method->getPlatformName()),
                uniqid()
            ));

            // TODO check uniqueness

            $this->persistenceHelper->persistAndRecompute($method);
        }
    }

    /**
     * Returns the shipment method from the event.
     *
     * @param ResourceEventInterface $event
     *
     * @return ShipmentMethodInterface
     * @throws InvalidArgumentException
     */
    private function getShipmentMethodFromEvent(ResourceEventInterface $event)
    {
        $resource = $event->getResource();

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

        return $resource;
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Model;

/**
 * Class StockSubjectStates
 * @package Ekyna\Component\Commerce\Stock\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class StockSubjectStates
{
    const STATE_IN_STOCK     = 'in_stock';
    const STATE_PRE_ORDER    = 'pre_order';
    const STATE_OUT_OF_STOCK = 'out_of_stock';

    /**
     * Returns all the states.
     *
     * @return array
     */
    static public function getStates()
    {
        return [
            static::STATE_IN_STOCK,
            static::STATE_PRE_ORDER,
            static::STATE_OUT_OF_STOCK,
        ];
    }

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

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

        // TODO assert valid states ?

        if ($stateA === static::STATE_IN_STOCK) {
            return $stateB !== static::STATE_IN_STOCK;
        } elseif ($stateA === static::STATE_PRE_ORDER) {
            return $stateB === static::STATE_OUT_OF_STOCK;
        }

        return false;
    }

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


<?php
namespace AuthActions\View\Helper;

use Cake\Utility\Hash;
use Cake\View\Helper;

class AuthHelper extends Helper {

	public $sessionKey = 'Auth.User';

	protected $_viewAuth;

/**
 * Configures the instance
 *
 * @param View $View CakePHP view object
 * @param array $config helper config
 */
	public function __construct(\Cake\View\View $View, array $config = array()) {
		parent::__construct($View, $config);

		$this->_viewAuth = $this->_View->get('viewAuthActions');
	}

/**
 * whether the user is logged in
 *
 * @return bool
 */
	public function loggedIn() {
		if ($this->_viewAuth) {
			return $this->sessionKey && $this->request->session()->check($this->sessionKey);
		}
		return false;
	}

/**
 * Accessor to the logged in user's properties
 *
 * @param string $key Key to read
 * @return mixed
 */
	public function user($key = null) {
		if ($this->sessionKey && $this->request->session()->check($this->sessionKey)) {
			$user = $this->request->session()->read($this->sessionKey);
		} else {
			return null;
		}
		if ($key === null) {
			return $user;
		}
		return Hash::get($user, $key);
	}

/**
 * Returns whether the user has the given $right
 *
 * @param string $right name of right
 * @return bool
 */
	public function hasRight($right) {
		$user = $this->user();
		if ($this->_viewAuth && !is_null($user)) {
			return $this->_viewAuth['UserRights']->userHasRight($user, $right);
		}
		return false;
	}

/**
 * Checks whether the URL is allowed for the currently logged in user
 *
 * @param string|array $url url to check
 * @return bool
 */
	public function urlAllowed($url) {
		if ($this->_viewAuth) {
			return $this->_viewAuth['AuthActions']->urlAllowed($this->user(), $url);
		}
		return false;
	}
}