<?php

namespace PeskyCMF\Scaffold;

use Illuminate\Http\JsonResponse;
use PeskyCMF\HttpCode;
use PeskyCMF\Scaffold\Form\FormConfig;
use PeskyCMF\Scaffold\Form\FormInput;
use PeskyORM\Core\DbExpr;
use PeskyORM\Exception\DbException;
use PeskyORM\Exception\InvalidDataException;
use PeskyORM\ORM\Record;
use PeskyORM\ORM\RecordInterface;
use PeskyORM\ORM\RecordValueHelpers;
use PeskyORM\ORM\TableInterface;
use PeskyORMLaravel\Db\Column\RecordPositionColumn;
use Symfony\Component\HttpFoundation\Response;

abstract class NormalTableScaffoldConfig extends ScaffoldConfig {

    public function getRecordsForDataGrid() {
        if (!$this->isSectionAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.access_denied_to_scaffold'));
        }
        $request = $this->getRequest();
        $dataGridConfig = $this->getDataGridConfig();
        $dataGridFilterConfig = $this->getDataGridFilterConfig();
        $conditions = [
            'LIMIT' => (int)$request->query('length', $dataGridConfig->getRecordsPerPage()),
            'OFFSET' => (int)$request->query('start', 0),
            'ORDER' => [],
        ];
        $conditions = array_merge($dataGridConfig->getSpecialConditions(), $conditions);
        $searchInfo = $request->query('search');
        if (!empty($searchInfo) && !empty($searchInfo['value'])) {
            $search = json_decode($searchInfo['value'], true);
            if (!empty($search) && is_array($search) && !empty($search['r'])) {
                $conditions = array_replace($dataGridFilterConfig->buildConditionsFromSearchRules($search), $conditions);
            }
        }
        if ($dataGridConfig->isNestedViewEnabled()) {
            // set parent id column value to null when nested view is enabled
            $parentPkValue = $request->get('parent');
            $conditions[$dataGridConfig->getColumnNameForNestedView()] = empty($parentPkValue) ? null : $parentPkValue;
        }
        $defaultOrderByColumn = $dataGridConfig->getOrderBy();
        $defaultOrderByDirection = $dataGridConfig->getOrderDirection();
        $defaultDirectionWithNulls = stripos($defaultOrderByDirection, ' nulls ') !== false;
        $order = $request->query('order', [[
            'column' => $defaultOrderByColumn,
            'dir' => $defaultOrderByDirection,
        ]]);
        $columns = $request->query('columns', array());
        /** @var array $order */
        foreach ($order as $config) {
            if (is_numeric($config['column']) && !empty($columns[$config['column']])) {
                $config['column'] = $columns[$config['column']]['name'];
            }
            if (!empty($config['column']) && !is_numeric($config['column'])) {
                if ($config['column'] instanceof DbExpr) {
                    $conditions['ORDER'][] = DbExpr::create($config['column']->get() . ' ' . $config['dir'], false);
                } else {
                    if (AbstractValueViewer::isComplexViewerName($config['column'])) {
                        list($colName, $keyName) = AbstractValueViewer::splitComplexViewerName($config['column']);
                        $conditions['ORDER'][] = DbExpr::create("`$colName`->>``$keyName`` {$config['dir']}", false);
                    } else if (
                        $defaultDirectionWithNulls
                        && $config['column'] === $defaultOrderByColumn
                        && stripos($defaultOrderByDirection, $config['dir']) !== false
                    ) {
                        $conditions['ORDER'][$config['column']] = $defaultOrderByDirection;
                    } else {
                        $conditions['ORDER'][$config['column']] = $config['dir'];
                    }

                    if ($dataGridConfig->hasValueViewer($config['column'])) {
                        $additionalOrders = $dataGridConfig->getValueViewer($config['column'])->getAdditionalOrderBy();
                        if (!empty($additionalOrders)) {
                            $conditions['ORDER'] = array_merge($conditions['ORDER'], $additionalOrders);
                        }
                    }
                }
            }
        }
        $dbColumns = static::getTable()->getTableStructure()->getColumns();
        $columnsToSelect = [];
        $virtualColumns = [];
        foreach (array_keys($dataGridConfig->getViewersLinkedToDbColumns(false)) as $originalColName) {
            list($colName, $keyName) = AbstractValueViewer::splitComplexViewerName($originalColName);
            if (array_key_exists($colName, $dbColumns)) {
                if ($dbColumns[$colName]->isItExistsInDb()) {
                    if ($keyName) {
                        $columnsToSelect[$originalColName] = DbExpr::create("`{$colName}`->>``{$keyName}``", false);
                    } else {
                        $columnsToSelect[] = $colName;
                    }
                } else {
                    $virtualColumns[] = implode('.', (array)$colName);
                }
            } else {
                throw new \UnexpectedValueException(
                    "Column '{$colName}' does not exist in " . get_class(static::getTable()->getTableStructure())
                );
            }
        }
        foreach ($dataGridConfig->getRelationsToRead() as $relationName => $columns) {
            if (is_int($relationName)) {
                $relationName = $columns;
                $columns = ['*'];
            }
            $columnsToSelect[$relationName] = $columns;
        }
        foreach ($dataGridConfig->getViewersForRelations() as $viewer) {
            if (!array_key_exists($viewer->getRelation()->getName(), $columnsToSelect)) {
                $columnsToSelect[$viewer->getRelation()->getName()] = ['*'];
            }
        }
        $result = static::getTable()->select($columnsToSelect, $conditions);
        $records = [];
        if ($result->count()) {
            $records = $dataGridConfig->prepareRecords($result->toArrays(), $virtualColumns);
        }
        return cmfJsonResponse()->setData([
            'draw' => $request->query('draw'),
            'recordsTotal' => $result->totalCount(),
            'recordsFiltered' => $result->totalCount(),
            'data' => $records,
        ]);
    }

    public function getRecordValues($id = null) {
        $isItemDetails = (bool)$this->getRequest()->query('details', false);
        $table = static::getTable();
        if ($isItemDetails) {
            $sectionConfig = $this->getItemDetailsConfig();
        } else {
            $sectionConfig = $this->getFormConfig();
        }
        if (
            ($isItemDetails && !$this->isDetailsViewerAllowed())
            || (!$isItemDetails && !$this->isEditAllowed())
        ) {
            return $this->makeAccessDeniedReponse(
                $sectionConfig->translateGeneral($isItemDetails ? 'message.forbidden' : 'message.edit.forbidden')
            );
        }
        $object = $table->newRecord();
        if (count($object::getPrimaryKeyColumn()->validateValue($id)) > 0) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $conditions = $sectionConfig->getSpecialConditions();
        $conditions[$table::getPkColumnName()] = $id;
        $relationsToRead = [];
        if ($id !== null) {
            foreach ($sectionConfig->getRelationsToRead() as $relationName => $columns) {
                if (is_int($relationName)) {
                    $relationName = $columns;
                    $columns = ['*'];
                }
                $relationsToRead[$relationName] = $columns;
            }
            foreach ($sectionConfig->getViewersForRelations() as $viewer) {
                if (!array_key_exists($viewer->getRelation()->getName(), $relationsToRead)) {
                    $relationsToRead[$viewer->getRelation()->getName()] = ['*'];
                }
            }
        }
        $columnsToSelect = $sectionConfig->getAdditionalColumnsToSelect();
        /** @var RenderableValueViewer $valueViewer */
        foreach ($sectionConfig->getViewersLinkedToDbColumns(false) as $valueViewer) {
            if ($valueViewer->getTableColumn()->isItExistsInDb()) {
                $columnsToSelect[] = $valueViewer->getTableColumn()->getName();
            }
        }
        if (!$object->fromDb($conditions, array_unique($columnsToSelect), array_keys($relationsToRead))->existsInDb()) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $this->logDbRecordLoad($object, static::getResourceName());
        $data = $object->toArray([], $relationsToRead, true);
        if (
            (
                $isItemDetails
                && !$this->isRecordDetailsAllowed($data)
            )
            ||
            (
                !$isItemDetails
                && !$this->isRecordEditAllowed($data)
            )
        ) {
            return $this->makeAccessDeniedReponse(
                $sectionConfig->translateGeneral($isItemDetails ? 'message.forbidden_for_record' : 'message.edit.forbidden_for_record')
            );
        }
        return cmfJsonResponse()->setData($sectionConfig->prepareRecord($data));
    }

    public function getDefaultValuesForFormInputs() {
        $formConfig = $this->getFormConfig();
        if (!$this->isCreateAllowed() && !$this->isEditAllowed()) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('message.create.forbidden'));
        }
        $data = $formConfig->alterDefaultValues(static::getTable()->newRecord()->getDefaults([], false, true));
        return cmfJsonResponse()->setData($formConfig->prepareRecord($data));
    }

    public function addRecord() {
        $formConfig = $this->getFormConfig();
        if (!$this->isCreateAllowed()) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('message.create.forbidden'));
        }
        $table = static::getTable();
        $data = $formConfig->modifyIncomingDataBeforeValidation(
            $this->getFilteredIncomingData($formConfig, true),
            true
        );
        unset($data[$table::getPkColumnName()]);
        $errors = $formConfig->validateDataForCreate($data);
        if (count($errors) !== 0) {
            return $this->makeValidationErrorsJsonResponse($errors);
        }
        if ($formConfig->hasBeforeSaveCallback()) {
            $data = call_user_func($formConfig->getBeforeSaveCallback(), true, $data, $formConfig);
            if (empty($data)) {
                throw new ScaffoldException('Empty $data received from beforeSave callback');
            }
            if ($formConfig->shouldRevalidateDataAfterBeforeSaveCallback(true)) {
                // revalidate
                $errors = $formConfig->validateDataForCreate($data, [], true);
                if (count($errors) !== 0) {
                    return $this->makeValidationErrorsJsonResponse($errors);
                }
            }
        }
        unset($data[$table::getPkColumnName()]); //< to be 100% sure =)
        if (!empty($data)) {
            try {
                $dataToSave = $this->getDataToSaveIntoMainRecord($data, $formConfig);
                $record = $table->newRecord()->fromData($dataToSave, false);
                $this->logDbRecordBeforeChange($record, static::getResourceName());
                $table::beginTransaction();
                $response = $this->doRecordSave($record, true);
                if ($response) {
                    if ($table::inTransaction()) {
                        $table::commitTransaction();
                    }
                    $this->logDbRecordAfterChange($record);
                    return $response;
                }
                $ret = $this->afterDataSaved($data, $record, true, $table, $formConfig);
                $this->logDbRecordAfterChange($record);
                return $ret;
            } catch (InvalidDataException $exc) {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                }
                return $this->makeValidationErrorsJsonResponse($exc->getErrors());
            } catch (\Throwable $exc) {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                }
                throw $exc;
            } finally {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                    /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                    throw new DbException('Transaction was not closed. Check if afterDataSaved method called and transaction is closed within it.');
                }
            }
        }
        throw new \BadMethodCallException('There is no data to save');
    }

    public function updateRecord() {
        $formConfig = $this->getFormConfig();
        if (!$this->isEditAllowed()) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('message.edit.forbidden'));
        }
        $table = static::getTable();
        $data = $formConfig->modifyIncomingDataBeforeValidation(
            $this->getFilteredIncomingData($formConfig, false),
            false
        );
        $errors = $formConfig->validateDataForEdit($data);
        if (count($errors) !== 0) {
            return $this->makeValidationErrorsJsonResponse($errors);
        }
        if (!$this->getRequest()->input($table::getPkColumnName())) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $id = $this->getRequest()->input($table::getPkColumnName());
        $record = $table->newRecord();
        if (count($record::getPrimaryKeyColumn()->validateValue($id)) > 0) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $conditions = $formConfig->getSpecialConditions();
        $conditions[$table::getPkColumnName()] = $id;
        if (!$record->fromDb($conditions)->existsInDb()) {
            return $this->makeRecordNotFoundResponse($table);
        }
        if (!$this->isRecordEditAllowed($record->toArrayWithoutFiles())) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('message.edit.forbidden_for_record'));
        }
        if ($formConfig->hasBeforeSaveCallback()) {
            $data = call_user_func($formConfig->getBeforeSaveCallback(), false, $data, $formConfig);
            if (empty($data)) {
                throw new ScaffoldException('Empty $data received from beforeSave callback');
            }
            if ($formConfig->shouldRevalidateDataAfterBeforeSaveCallback(false)) {
                // revalidate
                $errors = $formConfig->validateDataForEdit($data, [], true);
                if (count($errors) !== 0) {
                    return $this->makeValidationErrorsJsonResponse($errors);
                }
            }
        }
        unset($data[$table::getPkColumnName()]);
        if (!empty($data)) {
            try {
                $this->logDbRecordBeforeChange($record, static::getResourceName());
                $dataToSave = $this->getDataToSaveIntoMainRecord($data, $formConfig);
                $relationsData = array_intersect_key($dataToSave, $record::getRelations());
                $dataToSave = array_diff_key($dataToSave, $relationsData);
                $record->begin()->updateValues($dataToSave);
                $table::beginTransaction();
                $response = $this->doRecordSave($record, false);
                if ($response instanceof Response) {
                    if ($table::inTransaction()) {
                        $table::commitTransaction();
                    }
                    $this->logDbRecordAfterChange($record);
                    return $response;
                }
                $this->updateRelatedRecords($record, $relationsData);
                $ret = $this->afterDataSaved($data, $record, false, $table, $formConfig);
                $this->logDbRecordAfterChange($record);
                return $ret;
            } catch (InvalidDataException $exc) {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                }
                return $this->makeValidationErrorsJsonResponse($exc->getErrors());
            } catch (\Exception $exc) {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                }
                throw $exc;
            } finally {
                if ($table::inTransaction()) {
                    $table::rollBackTransaction();
                    /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
                    throw new DbException('Transaction was not closed. Check if afterDataSaved method called and transaction is closed within it.');
                }
            }
        }
        throw new \BadMethodCallException('There is no data to save');
    }

    /**
     * You may return instance of Response to immediately finish request (for example when validation error happens)
     * @param RecordInterface $record
     * @param bool $isCreation
     * @return null|Response
     */
    protected function doRecordSave(RecordInterface $record, bool $isCreation): ?Response {
        if ($isCreation) {
            $record->save(['*'], true);
        } else {
            $record->commit(['*'], true);
        }
        return null;
    }

    protected function getFilteredIncomingData(FormConfig $formConfig, bool $isCreation): array {
        $expectedDataKeys = [];
        /** @var FormInput $valueViewer */
        foreach ($formConfig->getValueViewers() as $valueViewer) {
            if (($isCreation && $valueViewer->isShownOnCreate()) || (!$isCreation && $valueViewer->isShownOnEdit())) {
                if ($valueViewer::isComplexViewerName($valueViewer->getName())) {
                    list($colName, ) = $valueViewer::splitComplexViewerName($valueViewer->getName());
                    $expectedDataKeys[] = $colName;
                } else {
                    $expectedDataKeys[] = $valueViewer->getName();
                }
            }
        }
        if (!$isCreation) {
            $expectedDataKeys[] = static::getTable()->getPkColumnName();
        }

        return $this->getRequest()->only($expectedDataKeys);
    }

    /**
     * @param Record $object
     * @param array $updatesForRelations
     * @throws InvalidDataException
     * @throws \PeskyORM\Exception\InvalidTableColumnConfigException
     * @throws \PeskyORM\Exception\OrmException
     */
    protected function updateRelatedRecords(Record $object, array $updatesForRelations) {
        foreach ($updatesForRelations as $relationName => $relationUpdates) {
            $relation = $object::getRelation($relationName);
            if ($relation->getType() === $relation::HAS_ONE) {
                $relatedRecord = $object->getRelatedRecord($relationName, true);
                $relationPk = RecordValueHelpers::normalizeValue(
                    array_get($relationUpdates, $relatedRecord::getPrimaryKeyColumnName()),
                    $relatedRecord::getPrimaryKeyColumn()->getType()
                );
                unset($relationUpdates[$relatedRecord::getPrimaryKeyColumnName()]);
                $relationUpdates[$relation->getForeignColumnName()] = $object->getPrimaryKeyValue();
                if ($relatedRecord->existsInDb()) {
                    if ($relationPk && $relationPk === $relatedRecord->getPrimaryKeyValue()) {
                        // same related record - just update
                        $relatedRecord->begin()->updateValues($relationUpdates, true)->commit();
                        continue;
                    } else if (!$relationPk || $relationPk !== $relatedRecord->getPrimaryKeyValue()) {
                        // related record changed - unset existing and proceed to attach new one
                        $relatedRecord
                            ->begin()
                            ->updateValue($relation->getForeignColumnName(), null, false)
                            ->commit();
                        $object->unsetRelatedRecord($relationName);
                        $relatedRecord->reset();
                    }
                }
                // attach new related record
                if ($relationPk) {
                    // related record already exists in DB but may belong to other record or to none - reattach it
                    $relatedRecord->reset()->fromPrimaryKey($relationPk);
                    unset($relationUpdates[$relatedRecord::getPrimaryKeyColumnName()]);
                    $relatedRecord->begin()->updateValues($relationUpdates, false)->commit();
                } else {
                    // create new related record and attach it
                    $relatedRecord->reset()->updateValues($relationUpdates, false)->save();
                }
            } else if ($relation->getType() === $relation::BELONGS_TO) {
                $relatedRecord = $object->getRelatedRecord($relationName, true);
                $relationPk = RecordValueHelpers::normalizeValue(
                    array_get($relationUpdates, $relatedRecord::getPrimaryKeyColumnName()),
                    $relatedRecord::getPrimaryKeyColumn()->getType()
                );
                unset($relationUpdates[$relatedRecord::getPrimaryKeyColumnName()]);
                if ($relatedRecord->existsInDb()) {
                    if (!$relationPk) {
                        $relationPk = $relatedRecord->getPrimaryKeyValue();
                    }
                    if ($relationPk === $relatedRecord->getPrimaryKeyValue()) {
                        // just update
                        $relatedRecord->begin()->updateValues($relationUpdates)->commit();
                        continue;
                    } else {
                        // related record changed - unset relation and continue to attach new 1
                        $object->unsetRelatedRecord($relationName);
                    }
                }
                $relatedRecord->reset();
                if (!$relationPk) {
                    // create new related record
                    $relatedRecord->updateValues($relationUpdates)->save();
                    $relationPk = $relatedRecord->getPrimaryKeyValue();
                } else {
                    // read existing related record from db and update it
                    $relatedRecord->reset()->fromPrimaryKey($relationPk);
                    $relatedRecord->begin()->updateValues($relationUpdates)->commit();
                }
                // attach new related record to this object
                $object->begin()->updateValue($relation->getLocalColumnName(), $relationPk, false)->commit();
            } else {
                // has many relations should work normally this way
                $object->begin()->updateRelatedRecord($relationName, $relationUpdates, false)->commit([$relationName], true);
            }
        }
    }

    /**
     * @param array $data
     * @param FormConfig $formConfig
     * @return array
     */
    protected function getDataToSaveIntoMainRecord(array $data, FormConfig $formConfig) {
        $viewersWithOwnValueSavingMethods = $formConfig->getInputsWithOwnValueSavingMethods();
        /** @var FormInput $viewer */
        foreach ($formConfig->getStandaloneViewers() as $key => $viewer) {
            array_forget($data, $key);
            if ($viewer->hasRelation() && empty($data[$viewer->getRelation()->getName()])) {
                unset($data[$viewer->getRelation()->getName()]);
            }
        }
        /** @var FormInput $viewer */
        foreach ($viewersWithOwnValueSavingMethods as $key => $viewer) {
            array_forget($data, $key);
            if ($viewer->hasRelation() && empty($data[$viewer->getRelation()->getName()])) {
                unset($data[$viewer->getRelation()->getName()]);
            }
        }
        return $data;
    }

    /**
     * @param array $data
     * @param RecordInterface $object
     * @param bool $created
     * @param TableInterface $table
     * @param FormConfig $formConfig
     * @return JsonResponse
     * @throws ScaffoldException
     */
    protected function afterDataSaved(array $data, RecordInterface $object, $created, TableInterface $table, FormConfig $formConfig) {
        foreach ($formConfig->getInputsWithOwnValueSavingMethods() as $formInput) {
            call_user_func($formInput->getValueSaver(), array_get($data, $formInput->getName()), $object, $created);
        }
        if ($formConfig->hasAfterSaveCallback()) {
            $success = call_user_func($formConfig->getAfterSaveCallback(), $created, $data, $object, $formConfig);
            if ($success instanceof \Symfony\Component\HttpFoundation\JsonResponse) {
                if ($success->getStatusCode() < 400) {
                    $table::commitTransaction();
                } else {
                    $table::rollBackTransaction();
                }
                return $success;
            } else if ($success !== true) {
                $table::rollBackTransaction();
                throw new ScaffoldException(
                    'afterSave callback must return true or instance of \Symfony\Component\HttpFoundation\JsonResponse'
                );
            }
        }
        $table::commitTransaction();
        $url = 'reload';
        if ($created && empty($this->getRequest()->input('create_another'))) {
            $url = routeToCmfItemEditForm(static::getResourceName(), $object->getPrimaryKeyValue());
        }
        return cmfJsonResponse()
            ->setMessage(
                $formConfig->translateGeneral($created ? 'message.create.success' : 'message.edit.success')
            )
            ->setRedirect($url);
    }

    public function updateBulkOfRecords() {
        $formConfig = $this->getFormConfig();
        if (!$this->isEditAllowed()) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('message.forbidden'));
        }
        $table = static::getTable();
        $data = $formConfig->modifyIncomingDataBeforeValidation(
            array_intersect_key($this->getRequest()->input(), $formConfig->getBulkEditableColumns()), //< do not use request->only() !!!
            false,
            true
        );
        if (empty($data)) {
            return cmfJsonResponse(HttpCode::INVALID)
                ->setMessage($formConfig->translateGeneral('bulk_edit.message.no_data_to_save'));
        }
        $errors = $formConfig->validateDataForBulkEdit($data);
        if (count($errors) !== 0) {
            return $this->makeValidationErrorsJsonResponse($errors);
        }
        if ($formConfig->hasBeforeBulkEditDataSaveCallback()) {
            $data = call_user_func($formConfig->getBeforeBulkEditDataSaveCallback(), $data, $formConfig);
            if (empty($data)) {
                throw new ScaffoldException('Empty $data received from beforeBulkEditDataSave callback');
            }
            if ($formConfig->shouldRevalidateDataAfterBeforeSaveCallback(false)) {
                // revalidate
                $errors = $formConfig->validateDataForBulkEdit($data, [], true);
                if (count($errors) !== 0) {
                    return $this->makeValidationErrorsJsonResponse($errors);
                }
            }
        }
        $conditions = $this->getSelectConditionsForBulkActions('_');
        if (!is_array($conditions)) {
            return $conditions; //< response
        }
        if (\Gate::denies('resource.update_bulk', [static::getResourceName(), $conditions])) {
            return $this->makeAccessDeniedReponse($formConfig->translateGeneral('bulk_edit.message.forbidden'));
        }
        $table::beginTransaction();
        $updatedCount = $table::update($data, $conditions);
        if ($formConfig->hasAfterBulkEditDataAfterSaveCallback()) {
            $success = call_user_func($formConfig->getAfterBulkEditDataAfterSaveCallback(), $data);
            if ($success instanceof \Symfony\Component\HttpFoundation\JsonResponse) {
                if ($success->getStatusCode() < 400) {
                    $table::commitTransaction();
                } else {
                    $table::rollBackTransaction();
                }
                return $success;
            } else if ($success !== true) {
                $table::rollBackTransaction();
                throw new ScaffoldException(
                    'afterBulkEditDataAfterSave callback must return true or instance of \Symfony\Component\HttpFoundation\JsonResponse'
                );
            }
        }
        $table::commitTransaction();
        $message = $updatedCount
            ? $formConfig->translateGeneral('bulk_edit.message.success', ['count' => $updatedCount])
            : $formConfig->translateGeneral('bulk_edit.message.nothing_updated');
        return cmfJsonResponse()
            ->setMessage($message)
            ->goBack(routeToCmfItemsTable(static::getResourceName()));
    }

    public function deleteRecord($id) {
        if (!$this->isDeleteAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.delete.forbidden'));
        }
        $table = static::getTable();
        $record = $table->newRecord();
        if (count($record::getPrimaryKeyColumn()->validateValue($id)) > 0) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $formConfig = $this->getFormConfig();
        $conditions = $formConfig->getSpecialConditions();
        $conditions[$table::getPkColumnName()] = $id;
        if (!$record->fromDb($conditions)->existsInDb()) {
            return $this->makeRecordNotFoundResponse($table);
        }
        if (!$this->isRecordDeleteAllowed($record->toArrayWithoutFiles())) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.delete.forbidden_for_record'));
        }
        $this->logDbRecordBeforeChange($record, static::getResourceName());
        $response = $this->doRecordDelete($record);
        $this->logDbRecordAfterChange($record);
        if ($response instanceof Response) {
            return $response;
        }

        return cmfJsonResponse()
            ->setMessage($this->translateGeneral('message.delete.success'))
            ->goBack(routeToCmfItemsTable(static::getResourceName()));
    }

    /**
     * You may return instance of Response to immediately finish request (for example when validation error happens)
     * @param RecordInterface $record
     * @return null|Response
     */
    protected function doRecordDelete(RecordInterface $record): ?Response {
        $record->delete();
        return null;
    }

    public function deleteBulkOfRecords() {
        if (!$this->isDeleteAllowed()) {
            return $this->makeAccessDeniedReponse($this->translateGeneral('message.delete.forbidden'));
        }
        $conditions = $this->getSelectConditionsForBulkActions();
        if (!is_array($conditions)) {
            return $conditions; //< response
        }
        if (\Gate::denies('resource.delete_bulk', [static::getResourceName(), $conditions])) {
            return $this->makeAccessDeniedReponse(
                $this->getDataGridConfig()->translateGeneral('bulk_actions.message.delete_bulk.forbidden')
            );
        }
        $deletedCount = static::getTable()->delete($conditions);
        $message = $deletedCount
            ? $this->getDataGridConfig()->translateGeneral('bulk_actions.message.delete_bulk.success', ['count' => $deletedCount])
            : $this->getDataGridConfig()->translateGeneral('bulk_actions.message.delete_bulk.nothing_deleted');
        return cmfJsonResponse()
            ->setMessage($message)
            ->goBack(routeToCmfItemsTable(static::getResourceName()));
    }

    /**
     * @param string $inputNamePrefix - input name prefix
     *      For example if you use '_ids' instead of 'ids' - use prefix '_'
     * @return array|Response
     * @throws \UnexpectedValueException
     * @throws \PeskyORM\Exception\OrmException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    protected function getSelectConditionsForBulkActions($inputNamePrefix = '') {
        $formConfig = $this->getFormConfig();
        $specialConditions = $formConfig->getSpecialConditions();
        $conditions = $specialConditions;
        $idsField = $inputNamePrefix . 'ids';
        $conditionsField = $inputNamePrefix . 'conditions';
        if ($this->getRequest()->has($idsField)) {
            $this->validate($this->getRequest(), [
                $idsField => 'required|array',
                $idsField . '.*' => 'integer|min:1',
            ]);
            $conditions[static::getTable()->getPkColumnName()] = $this->getRequest()->input($idsField);
        } else if ($this->getRequest()->has($conditionsField)) {
            $this->validate($this->getRequest(), [
                $conditionsField => 'string|regex:%^[\{\[].*[\}\]]$%s',
            ]);
            $encodedConditions = $this->getRequest()->input($conditionsField) !== ''
                ? json_decode($this->getRequest()->input($conditionsField), true)
                : [];
            if ($encodedConditions === false || !is_array($encodedConditions) || empty($encodedConditions['r'])) {
                return cmfJsonResponseForValidationErrors(
                    [$conditionsField => 'JSON expected'],
                    $formConfig->translateGeneral('message.validation_errors')
                );
            }
            if (!empty($encodedConditions)) {
                $filterConditions = $this
                    ->getDataGridFilterConfig()
                    ->buildConditionsFromSearchRules($encodedConditions);
                $conditions = array_merge($filterConditions, $specialConditions);
            }
        } else {
            return cmfJsonResponseForValidationErrors(
                [
                    $idsField => 'List of items IDs of filtering conditions expected',
                    $conditionsField => 'List of items IDs of filtering conditions expected',
                ],
                $formConfig->translateGeneral('message.validation_errors')
            );
        }
        return $conditions;
    }

    public function changeItemPosition($id, $beforeOrAfter, $otherId, $columnName, $sortDirection) {
        $dataGridConfig = $this->getDataGridConfig();
        if (
            !$dataGridConfig->isRowsReorderingEnabled()
            || !in_array($columnName, $dataGridConfig->getRowsPositioningColumns(), true)
        ) {
            return $this->makeAccessDeniedReponse($dataGridConfig->translateGeneral('message.change_position.forbidden'));
        }
        $table = static::getTable();
        if (
            count($table::getPkColumn()->validateValue($id)) > 0
            || count($table::getPkColumn()->validateValue($otherId)) > 0
        ) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $specialConditions = $dataGridConfig->getSpecialConditions();
        /** @var RecordPositionColumn $columnConfig */
        $columnConfig = static::getTable()->getTableStructure()->getColumn($columnName);
        $movedRecord = $table::getInstance()
            ->newRecord()
            ->fromDb(array_merge($specialConditions, [$table::getPkColumnName() => $id]));
        if (!$movedRecord->existsInDb()) {
            return $this->makeRecordNotFoundResponse($table);
        }
        $position1 = $table::selectValue(
            DbExpr::create("`$columnName`"),
            array_merge($specialConditions, [$table::getPkColumnName() => $otherId])
        );
        if ($position1 === null) {
            // this value must be present to continue
            return $this->makeRecordNotFoundResponse($table);
        }
        $isFloat = $columnConfig->getType() === $columnConfig::TYPE_FLOAT;
        $position1 = $isFloat ? (float)$position1 : (int)$position1;
        // We have next situations here:
        // 1. $sortDirection = 'asc', $beforeOrAfter = 'before':
        //      We need to find out previous record's position using ORDER BY $columnName ASC and <= equation on $columnName
        // 2. $sortDirection = 'asc', $beforeOrAfter = 'after':
        //      We need to find out next record's position using ORDER BY $columnName ASC and <= equation on $columnName
        // 3. $sortDirection = 'desc', $beforeOrAfter = 'before':
        //      same as 2
        // 4. $sortDirection = 'desc', $beforeOrAfter = 'after':
        //      same as 1
        $sortDirection = strtolower($sortDirection);
        $beforeOrAfter = strtolower($beforeOrAfter);
        $isAscending = ($sortDirection === 'asc' && $beforeOrAfter === 'before') || ($sortDirection === 'desc' && $beforeOrAfter === 'after');
        $position2 = $table::selectValue(
            DbExpr::create("`$columnName`"),
            [
                $columnName . ($isAscending ? ' <=' : ' >=') => $position1,
                $table::getPkColumnName() . ' !=' => [$otherId, $movedRecord->getPrimaryKeyValue()],
                'ORDER' => [$columnName => 'ASC'],
            ]
        );
        $increment = $columnConfig instanceof RecordPositionColumn ? $columnConfig->getIncrement() : 100;
        if ($position2 === null) {
            $position2 = $isAscending ? 0 : ($position1 + $increment * 2);
        } else {
            $position2 = $isFloat ? (float)$position2 : (int)$position2;
        }
        // calculate distance between records to find out if there is any space to add $movedRecord between them
        $distance = abs($position1 - $position2);
        $table::beginTransaction();
        if ($distance > 1) {
            $newPosition = min($position1, $position2) + ($isFloat ? (float)($distance / 2) : (int)($distance / 2));
        } else {
            // no free space - shift other records and insert $movedRecord to freed space
            $table::update(
                [$columnName => DbExpr::create("`{$columnName}` + ``{$increment}`` + ``{$increment}``")],
                [
                    $columnName . '>=' => max($position1, $position2),
                ]
            );
            $newPosition = max($position1, $position2) + $increment;
        }
        $movedRecord->begin()->updateValue($columnConfig, $newPosition, false)->commit();
        $table::commitTransaction();
        return cmfJsonResponse()
            ->setMessage($dataGridConfig->translateGeneral('message.change_position.success'));
    }

}


<?php

namespace Ekyna\Component\Commerce\Subject\Entity;

/**
 * Class SubjectIdentity
 * @package Ekyna\Component\Commerce\Subject\Entity
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
final class SubjectIdentity
{
    /**
     * @var string
     */
    private $provider;

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

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

    /**
     * Returns whether or not the subject identity is set.
     *
     * @return bool
     */
    public function hasIdentity()
    {
        return !empty($this->provider) && !empty($this->identifier);
    }

    /**
     * Clears the subject identity.
     */
    public function clear()
    {
        $this->provider = null;
        $this->identifier = null;
        $this->subject = null;
    }

    /**
     * Returns whether or not this subject identity equals the given one.
     *
     * @param SubjectIdentity $identity
     *
     * @return bool
     */
    public function equals(SubjectIdentity $identity)
    {
        return $this->provider === $identity->getProvider()
            && $this->identifier === $identity->getIdentifier();
    }

    /**
     * Copy the given subject identity.
     *
     * @param SubjectIdentity $identity
     */
    public function copy(SubjectIdentity $identity)
    {
        $this->provider = $identity->getProvider();
        $this->identifier = $identity->getIdentifier();
    }

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

    /**
     * Sets the provider.
     *
     * @param string $provider
     *
     * @return SubjectIdentity
     */
    public function setProvider($provider)
    {
        $this->provider = (string)$provider;

        return $this;
    }

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

    /**
     * Sets the identifier.
     *
     * @param string $identifier
     *
     * @return SubjectIdentity
     */
    public function setIdentifier($identifier)
    {
        $this->identifier = (string)$identifier;

        return $this;
    }

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

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

        return $this;
    }
}


<?php

/**
 * This file is part of MetaModels/attribute_translatedtags.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/attribute_translatedtags
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/attribute_translatedtags/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\AttributeTranslatedTagsBundle\ContaoManager;

use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
use MetaModels\AttributeTagsBundle\MetaModelsAttributeTagsBundle;
use MetaModels\AttributeTranslatedTagsBundle\MetaModelsAttributeTranslatedTagsBundle;
use MetaModels\CoreBundle\MetaModelsCoreBundle;

/**
 * Contao Manager plugin.
 */
class Plugin implements BundlePluginInterface
{
    /**
     * {@inheritdoc}
     */
    public function getBundles(ParserInterface $parser)
    {
        return [
            BundleConfig::create(MetaModelsAttributeTranslatedTagsBundle::class)
                ->setLoadAfter(
                    [
                        MetaModelsCoreBundle::class,
                        MetaModelsAttributeTagsBundle::class,
                    ]
                )
                ->setReplace(['metamodelsattribute_translatedtags'])
        ];
    }
}


<?php

namespace mii\core;

class ACL
{

    /**
     * @var array ACL rules
     */
    public $_rules = [];

    /**
     * Add "allow" access to a role or roles.
     *
     * @param mixed $role single role or array of roles
     * @param string $action
     * @return $this
     */
    public function allow($role, $action = '*') {
        $this->add_rule(true, $role, $action);

        return $this;
    }

    /**
     * Add "deny" access to a role or roles.
     *
     * @param mixed $role single role or array of roles
     * @param string $action
     * @return $this
     */
    public function deny($role, $action = '*') {
        $this->add_rule(false, $role, $action);

        return $this;
    }

    /**
     * Clear array of ACL rules.
     *
     * @return $this
     */
    public function clear() {
        $this->_rules = [];

        return $this;
    }

    /**
     * Add rule to array of ACL rules.
     *
     * @param bool $access
     * @param mixed $role single role or array of roles
     * @param string $action
     */
    public function add_rule($access, $role, $action): void {
        $roles = (array)$role;

        foreach ($roles as $r) {
            $action = (array)$action;
            foreach ($action as $a) {
                $this->_rules[$r][$a] = $access;
            }
        }
    }

    /**
     * Check if a role (or one of roles) is allowed to an action.
     *
     * @param mixed $role single role or array of roles
     * @param string $action
     * @return bool
     */
    public function check($role, $action = '*'): bool {
        if (is_array($role)) {
            foreach ($role as $r) {
                if ($this->match($r, $action))
                    return true;
            }

            return false;
        }

        return $this->match($role, $action);
    }

    /**
     * Check if a role is allowed to an action.
     *
     * @param $role
     * @param $action
     * @return bool
     */
    protected function match($role, $action): bool {
        $roles = $actions = ['*'];

        $allow = false;

        if ($role != '*')
            array_unshift($roles, $role);
        if ($action != '*')
            array_unshift($actions, $action);

        // Ищем наиболее подходящее правило. Идем от частного к общему.
        foreach ($roles as $_role) {
            foreach ($actions as $_action) {
                if (isset($this->_rules[$_role][$_action])) {
                    $allow = $this->_rules[$_role][$_action];
                    break 2;
                }
            }
        }

        return $allow === true;
    }
}


<?php

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

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

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

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

        if (isset($model[Constants::FIELD_STATUS])) {
            return;
        }
        if (!isset($model[Constants::FIELD_AMOUNT], $model[Constants::FIELD_BALANCE])) {
            throw new RuntimeException("Payment has not been converted.");
        }

        if ($model[Constants::FIELD_AMOUNT] <= $model[Constants::FIELD_BALANCE]) {
            $model[Constants::FIELD_STATUS] = Constants::STATUS_CAPTURED;
        } else {
            $model[Constants::FIELD_STATUS] = Constants::STATUS_FAILED;
        }
    }

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


<?php

namespace Ekyna\Component\Commerce\Stock\Updater;

use Ekyna\Component\Commerce\Stock\Model\StockSubjectModes;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectStates;
use Ekyna\Component\Commerce\Stock\Model\StockSubjectInterface;
use Ekyna\Component\Commerce\Stock\Model\StockUnitStates;
use Ekyna\Component\Commerce\Stock\Resolver\StockUnitResolverInterface;
use Ekyna\Component\Commerce\Subject\Model\SubjectInterface;
use Ekyna\Component\Commerce\Supplier\Repository\SupplierProductRepositoryInterface;

/**
 * Class StockSubjectUpdater
 * @package Ekyna\Component\Commerce\Stock\Updater
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class StockSubjectUpdater implements StockSubjectUpdaterInterface
{
    /**
     * @var StockUnitResolverInterface
     */
    protected $stockUnitResolver;

    /**
     * @var SupplierProductRepositoryInterface
     */
    protected $supplierProductRepository;

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

    /**
     * @inheritdoc
     */
    public function update(StockSubjectInterface $subject)
    {
        // If subject stock is compound, do nothing.
        if ($subject->isStockCompound()) {
            // TODO stock inheritance system
            return false;
        }

        // If subject stock mode is "disabled", reset to zero and set state to "In stock".
        if ($subject->getStockMode() === StockSubjectModes::MODE_DISABLED) {
            $changed = $this->setSubjectData($subject);
            $changed |= $this->setSubjectState($subject, StockSubjectStates::STATE_IN_STOCK);
            return $changed;
        }

        $ordered = $received = $adjusted = $sold = $shipped = 0;
        $eda = null;

        // Loop over the 'not closed' stock units.
        // We don't use a dql query to get sums, because database may not reflect
        // real/current data of theses stock units (during a flush event).
        // The stock unit resolver uses the stock unit cache.
        $stockUnits = $this->stockUnitResolver->findNotClosed($subject);
        foreach ($stockUnits as $stockUnit) {
            $sold += $stockUnit->getSoldQuantity();
            $shipped += $stockUnit->getShippedQuantity();
            $adjusted += $stockUnit->getAdjustedQuantity();

            if ($stockUnit->getState() !== StockUnitStates::STATE_NEW) {
                $ordered += $o = $stockUnit->getOrderedQuantity();
                $received += $r = $stockUnit->getReceivedQuantity();

                // Ignore EDA if stock unit his fully received
                if (0 < $o && 0 < $r && $r >= $o) {
                    continue;
                }

                // Skip null EDA
                if (null === $date = $stockUnit->getEstimatedDateOfArrival()) {
                    continue;
                }

                // Keep lowest EDA
                if (null === $eda || $eda > $date) {
                    $eda = $date;
                }
            }
        }

        // In stock
        $inStock = $received + $adjusted - $shipped;
        if (0 > $inStock) $inStock = 0;

        // Available stock
        $availableStock = $received + $adjusted - $sold;
        if (0 > $availableStock) $availableStock = 0;

        // Virtual stock
        $virtualStock = $ordered + $adjusted - $sold;

        // Estimated date of arrival
        // Set null if we do not expect supplier deliveries
        if ($ordered <= $received) {
            $eda = null;
        }

        // Supplier product min eda for "auto" and "just in time" subjects
        if (
            null === $eda &&
            $subject->getStockMode() !== StockSubjectModes::MODE_MANUAL &&
            $subject instanceof SubjectInterface
        ) {
            $eda = $this->supplierProductRepository->getMinEstimatedDateOfArrivalBySubject($subject);
        }

        $eda = $this->nullDateIfLowerThanToday($eda);

        $changed = $this->setSubjectData($subject, $inStock, $availableStock, $virtualStock, $eda);

        $changed |= $this->updateStockState($subject);

        return $changed;
    }

    /**
     * @inheritdoc
     */
    public function updateStockState(StockSubjectInterface $subject)
    {
        // If subject stock is compound, do nothing.
        if ($subject->isStockCompound()) {
            // TODO stock inheritance system
            return false;
        }

        $mode = $subject->getStockMode();

        // If subject stock mode is "disabled" -> "In stock" state.
        if ($subject->getStockMode() === StockSubjectModes::MODE_DISABLED) {
            return $this->setSubjectState($subject, StockSubjectStates::STATE_IN_STOCK);
        }

        $state = StockSubjectStates::STATE_OUT_OF_STOCK;
        $min = $subject->getMinimumOrderQuantity();

        // If subject has available stock -> "In stock" state
        if ($min <= $subject->getAvailableStock()) {
            $state = StockSubjectStates::STATE_IN_STOCK;
        }
        // Else if subject has virtual stock and estimated date of arrival -> "Pre order" state
        elseif ($min <= $subject->getVirtualStock() && null !== $subject->getEstimatedDateOfArrival()) {
            $state = StockSubjectStates::STATE_PRE_ORDER;
        }
        // Else if stock mode is "Auto" or "Just in time"
        elseif ($mode !== StockSubjectModes::MODE_MANUAL) {
            $available = 0;
            $ordered = 0;

            // Supplier product availability
            if ($subject instanceof SubjectInterface) {
                $available = $this->supplierProductRepository->getAvailableQuantitySumBySubject($subject);
                // TODO $ordered = $this->supplierProductRepository->getOrderedQuantitySumBySubject($subject);
            }

            $eda = $this->nullDateIfLowerThanToday($subject->getEstimatedDateOfArrival());

            // If suppliers has available stock or is about to get it
            if ($min <= $available || ($min <= $ordered && null !== $eda)) {
                $state = StockSubjectStates::STATE_PRE_ORDER;
            }
        }

        // If "Just in time" mode
        if ($mode === StockSubjectModes::MODE_JUST_IN_TIME) {
            // If "out of stock" state
            if ($state === StockSubjectStates::STATE_OUT_OF_STOCK) {
                // Fallback to "Pre order" state
                $state = StockSubjectStates::STATE_PRE_ORDER;
            }
            // Else if "pre order" state
            elseif($state === StockSubjectStates::STATE_PRE_ORDER) {
                // Fallback to "In stock" state
                $state = StockSubjectStates::STATE_IN_STOCK;
            }
        }

        return $this->setSubjectState($subject, $state);
    }

    /**
     * Returns the given date or null if it's lower than today.
     *
     * @param \DateTime|null $eda
     *
     * @return \DateTime|null
     */
    private function nullDateIfLowerThanToday(\DateTime $eda = null)
    {
        if (null === $eda) {
            return null;
        }

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

        if ($eda < $today) {
            return null;
        }

        return $eda;
    }

    /**
     * Sets the subject stock data.
     *
     * @param StockSubjectInterface $subject
     * @param float                 $inStock
     * @param float                 $availableStock
     * @param float                 $virtualStock
     * @param \DateTime|null        $eda
     *
     * @return bool Whether the data has been changed.
     */
    private function setSubjectData(
        StockSubjectInterface $subject,
        $inStock = .0,
        $availableStock = .0,
        $virtualStock = .0,
        \DateTime $eda = null
    ) {
        $changed = false;

        if ($inStock != $subject->getInStock()) { // TODO use packaging format (bccomp for float)
            $subject->setInStock($inStock);
            $changed = true;
        }

        if ($availableStock != $subject->getAvailableStock()) { // TODO use packaging format (bccomp for float)
            $subject->setAvailableStock($availableStock);
            $changed = true;
        }

        if ($virtualStock != $subject->getVirtualStock()) { // TODO use packaging format (bccomp for float)
            $subject->setVirtualStock($virtualStock);
            $changed = true;
        }

        if ($eda !== $subject->getEstimatedDateOfArrival()) {
            $subject->setEstimatedDateOfArrival($eda);
            $changed = true;
        }

        return $changed;
    }

    /**
     * Sets the subject's stock state.
     *
     * @param StockSubjectInterface $subject
     * @param string                $state
     *
     * @return bool Whether the state has been changed.
     */
    private function setSubjectState(StockSubjectInterface $subject, $state)
    {
        if ($subject->getStockState() != $state) {
            $subject->setStockState($state);

            return true;
        }

        return false;
    }
}


<?php

namespace mii\db;

/**
 * Database expressions can be used to add unescaped SQL fragments to a
 * [Query] object.
 *
 * @copyright  (c) 2015 Lev Morozov
 * @copyright  (c) 2009 Kohana Team
 */

class Expression
{

    // Unquoted parameters
    protected $_parameters;

    // Raw expression string
    protected $_value;

    /**
     * Sets the expression string.
     *
     *     $expression = new Expression('COUNT(users.id)');
     *
     * @param   string $value raw SQL expression string
     * @param   array $parameters unquoted parameter values
     * @return  void
     */
    public function __construct($value, $parameters = array()) {
        // Set the expression string
        $this->_value = $value;
        $this->_parameters = $parameters;
    }

    /**
     * Bind a variable to a parameter.
     *
     * @param   string $param parameter key to replace
     * @param   mixed $var variable to use
     * @return  $this
     */
    public function bind($param, & $var) {
        $this->_parameters[$param] =& $var;

        return $this;
    }

    /**
     * Set the value of a parameter.
     *
     * @param   string $param parameter key to replace
     * @param   mixed $value value to use
     * @return  $this
     */
    public function param($param, $value) {
        $this->_parameters[$param] = $value;

        return $this;
    }

    /**
     * Add multiple parameter values.
     *
     * @param   array $params list of parameter values
     * @return  $this
     */
    public function parameters(array $params) {
        $this->_parameters = $params + $this->_parameters;

        return $this;
    }

    /**
     * Get the expression value as a string.
     *
     *     $sql = $expression->value();
     *
     * @return  string
     */
    public function value(): string {
        return (string)$this->_value;
    }

    /**
     * Return the value of the expression as a string.
     *
     *     echo $expression;
     *
     * @return  string
     * @uses    Database_Expression::value
     */
    public function __toString() {
        return $this->value();
    }

    /**
     * Compile the SQL expression and return it. Replaces any parameters with
     * their given values.
     *
     * @param   mixed    Database instance or name of instance
     * @return  string
     */
    public function compile(Database $db = NULL): string {
        if ($db === null) {
            // Get the database instance
            $db = \Mii::$app->db;
        }

        $value = $this->value();

        if (!empty($this->_parameters)) {
            // Quote all of the parameter values
            $params = array_map([$db, 'quote'], $this->_parameters);

            // Replace the values in the expression
            $value = strtr($value, $params);
        }

        return $value;
    }

}


<?php

declare(strict_types=1);

namespace FondBot\Drivers\Telegram;

use FondBot\Templates\Keyboard;
use FondBot\Drivers\TemplateCompiler;
use FondBot\Templates\Keyboard\UrlButton;
use FondBot\Templates\Keyboard\PayloadButton;
use unreal4u\TelegramAPI\Telegram\Types\KeyboardButton;
use unreal4u\TelegramAPI\Telegram\Types\ReplyKeyboardMarkup;
use unreal4u\TelegramAPI\Telegram\Types\Inline\Keyboard\Button as InlineKeyboardButton;
use unreal4u\TelegramAPI\Telegram\Types\Inline\Keyboard\Markup as InlineKeyboardMarkup;

class TelegramTemplateCompiler extends TemplateCompiler
{
    /**
     * Compile keyboard.
     *
     * @param Keyboard $keyboard
     *
     * @return mixed
     */
    protected function compileKeyboard(Keyboard $keyboard)
    {
        $firstButton = collect($keyboard->getButtons())->first();

        if ($firstButton instanceof PayloadButton || $firstButton instanceof UrlButton) {
            return $this->compileInlineKeyboard($keyboard);
        }

        return $this->compileReplyKeyboard($keyboard);
    }

    protected function compileInlineKeyboard(Keyboard $keyboard): InlineKeyboardMarkup
    {
        $inlineKeyboardMarkup = new InlineKeyboardMarkup();

        foreach ($keyboard->getButtons() as $button) {
            $inlineKeyboardButton = new InlineKeyboardButton();
            $inlineKeyboardButton->text = $button->getLabel();
            $inlineKeyboardButton->switch_inline_query = $button->getParameters()->get('switch_inline_query');
            $inlineKeyboardButton->switch_inline_query_current_chat = $button->getParameters()->get('switch_inline_query_current_chat');
            $inlineKeyboardButton->pay = $button->getParameters()->get('pay');

            if ($button instanceof PayloadButton) {
                $inlineKeyboardButton->callback_data = $button->getPayload();
            }

            if ($button instanceof UrlButton) {
                $inlineKeyboardButton->url = $button->getUrl();
            }

            $inlineKeyboardMarkup->inline_keyboard[] = [$inlineKeyboardButton];
        }

        return $inlineKeyboardMarkup;
    }

    protected function compileReplyKeyboard(Keyboard $keyboard): ReplyKeyboardMarkup
    {
        $replyKeyboardMarkup = new ReplyKeyboardMarkup();
        $replyKeyboardMarkup->resize_keyboard = $keyboard->getParameters()->get('resize_keyboard', true);
        $replyKeyboardMarkup->one_time_keyboard = $keyboard->getParameters()->get('one_time_keyboard', true);

        foreach ($keyboard->getButtons() as $button) {
            $keyboardButton = new KeyboardButton();
            $keyboardButton->text = $button->getLabel();
            $keyboardButton->request_contact = $button->getParameters()->get('request_contact', false);
            $keyboardButton->request_location = $button->getParameters()->get('request_location', false);

            $replyKeyboardMarkup->keyboard[] = [$keyboardButton];
        }

        return $replyKeyboardMarkup;
    }
}


<?php

namespace Ekyna\Component\Commerce\Common\View;

use Ekyna\Component\Commerce\Common\Calculator\AmountCalculatorInterface;
use Ekyna\Component\Commerce\Common\Calculator\MarginCalculatorInterface;
use Ekyna\Component\Commerce\Common\Model;
use Ekyna\Component\Commerce\Common\Util\Formatter;
use Ekyna\Component\Commerce\Common\Util\FormatterFactory;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class ViewBuilder
 * @package Ekyna\Component\Commerce\Common\View
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ViewBuilder
{
    /**
     * @var ViewTypeRegistryInterface
     */
    private $registry;

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

    /**
     * @var MarginCalculatorInterface
     */
    private $marginCalculator;

    /**
     * @var FormatterFactory
     */
    private $formatterFactory;

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

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

    /**
     * @var OptionsResolver
     */
    private $optionsResolver;

    /**
     * @var array
     */
    private $options;

    /**
     * @var SaleView
     */
    private $view;

    /**
     * @var ViewTypeInterface[]
     */
    private $types;

    /**
     * @var int
     */
    private $lineNumber;

    /**
     * @var Formatter
     */
    private $formatter;

    /**
     * Constructor.
     *
     * @param ViewTypeRegistryInterface $registry
     * @param AmountCalculatorInterface $amountCalculator
     * @param MarginCalculatorInterface $marginCalculator
     * @param FormatterFactory          $formatterFactory
     * @param string                    $defaultTemplate
     * @param string                    $editableTemplate
     */
    public function __construct(
        ViewTypeRegistryInterface $registry,
        AmountCalculatorInterface $amountCalculator,
        MarginCalculatorInterface $marginCalculator,
        FormatterFactory $formatterFactory,
        $defaultTemplate = '@Commerce/Sale/view.html.twig',
        $editableTemplate = '@Commerce/Sale/view_editable.html.twig'
    ) {
        $this->registry = $registry;
        $this->amountCalculator = $amountCalculator;
        $this->marginCalculator = $marginCalculator;
        $this->formatterFactory = $formatterFactory;
        $this->defaultTemplate = $defaultTemplate;
        $this->editableTemplate = $editableTemplate;
    }

    /**
     * Builds the sale view.
     *
     * @param Model\SaleInterface $sale
     * @param array               $options
     *
     * @return SaleView
     */
    public function buildSaleView(Model\SaleInterface $sale, array $options = [])
    {
        $this->initialize($sale, $options);

        $this->amountCalculator->calculateSale($sale);

        // Gross total view
        $grossResult = $sale->getGrossResult();
        $this->view->setGross(new TotalView(
            $this->formatter->currency($grossResult->getGross($this->view->isAti())),
            $this->formatter->currency($grossResult->getDiscount($this->view->isAti())),
            $this->formatter->currency($grossResult->getBase($this->view->isAti()))
        ));

        // Final total view
        $finalResult = $sale->getFinalResult();
        $this->view->setFinal(new TotalView(
            $this->formatter->currency($finalResult->getBase()),
            $this->formatter->currency($finalResult->getTax()),
            $this->formatter->currency($finalResult->getTotal())
        ));

        if ($this->options['private'] && null !== $margin = $this->marginCalculator->calculateSale($sale)) {
            $prefix = $margin->isAverage() ? '~' : '';
            $this->view->setMargin(new MarginView(
                $prefix . $this->formatter->currency($margin->getAmount()),
                $prefix . $this->formatter->percent($margin->getPercent())
            ));
            $this->view->vars['show_margin'] = true;
        }

        // Items lines
        $this->buildSaleItemsLinesViews($sale);
        // Discounts lines
        $this->buildSaleDiscountsLinesViews($sale);
        // Shipment line
        $this->buildShipmentLine($sale);
        // Taxes views
        $this->buildSaleTaxesViews($sale);

        // Extends
        foreach ($this->types as $type) {
            $type->buildSaleView($sale, $this->view, $this->options);
        }

        $columnsCount = 6;
        if ($this->view->vars['show_availability']) {
            $columnsCount++;
        }
        if ($this->view->vars['show_discounts'] = 0 < count($grossResult->getDiscountAdjustments())) {
            $columnsCount += 3;
        }
        if ($this->view->vars['show_taxes'] = 1 < count($finalResult->getTaxAdjustments())) {
            $columnsCount++;
        }
        if ($this->view->vars['show_margin']) {
            $columnsCount++;
        }
        if ($this->options['editable']) {
            $columnsCount++;
        }
        $this->view->vars['columns_count'] = $columnsCount;

        return $this->view;
    }

    /**
     * Initializes the view builder.
     *
     * @param Model\SaleInterface $sale
     * @param array               $options
     */
    private function initialize(Model\SaleInterface $sale, array $options = [])
    {
        $this->lineNumber = 1;
        $this->view = new SaleView();

        $this->types = $this->registry->getTypesForSale($sale);

        foreach ($this->types as $type) {
            $type->configureOptions($sale, $this->view, $options);
        }

        $this->options = $this->getOptionsResolver()->resolve($options);

        $this->view->setTemplate($this->options['template']);

        if (!is_null($this->options['ati'])) {
            $this->view->setAti($this->options['ati']);
        } else {
            $this->view->setAti($sale->isAtiDisplayMode());
        }

        $this->formatter = $this->formatterFactory->create($this->options['locale'], $sale->getCurrency()->getCode());

        foreach ($this->types as $type) {
            $type->setFormatter($this->formatter);
        }
    }

    /**
     * Builds the sale taxes views.
     *
     * @param Model\SaleInterface $sale
     */
    private function buildSaleTaxesViews(Model\SaleInterface $sale)
    {
        if (!$this->options['taxes_view']) {
            return;
        }

        $amounts = $this->amountCalculator->calculateSale($sale);

        foreach ($amounts->getTaxAdjustments() as $tax) {
            $this->view->addTax(new TaxView(
                $tax->getName(),
                $this->formatter->currency($tax->getAmount())
            ));
        }
    }

    /**
     * Builds the sale lines views.
     *
     * @param Model\SaleInterface $sale
     */
    private function buildSaleItemsLinesViews(Model\SaleInterface $sale)
    {
        if (!$sale->hasItems()) {
            return;
        }

        foreach ($sale->getItems() as $item) {
            // We don't need to test if null is returned as root items can't be private.
            $this->view->addItem($this->buildSaleItemLineView($item));
        }
    }

    /**
     * Builds the sale discounts lines views.
     *
     * @param Model\SaleInterface $sale
     */
    private function buildSaleDiscountsLinesViews(Model\SaleInterface $sale)
    {
        if (!$sale->hasAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT)) {
            return;
        }

        foreach ($sale->getAdjustments(Model\AdjustmentTypes::TYPE_DISCOUNT) as $adjustment) {
            $this->view->addDiscount($this->buildDiscountLine($adjustment));
        }
    }

    /**
     * Builds the sale line view.
     *
     * @param Model\SaleItemInterface $item
     * @param int                     $level
     *
     * @return LineView|null
     */
    private function buildSaleItemLineView(Model\SaleItemInterface $item, $level = 0)
    {
        if (!$this->options['private'] && $item->isPrivate()) {
            return null;
        }

        $lineNumber = $this->lineNumber++;

        $view = new LineView(
            'item_' . ($lineNumber - 1),
            'item_' . $item->getId(),
            $lineNumber,
            $level
        );

        $view->addClass('level-' . $level);

        $result = $item->getResult();

        $unit = $gross = $discountRates = $discountAmount = $base = $taxRates = $taxAmount = $total = null;

        if (!($item->isCompound() && !$item->hasPrivateChildren())) {
            $ati = $this->view->isAti();
            $unit = $this->formatter->currency($result->getUnit($ati));
            $gross = $this->formatter->currency($result->getGross($ati));
            $discountRates = $this->formatter->rates(...$result->getDiscountAdjustments());
            if (0 < $discount = $result->getDiscount($ati)) {
                $discountAmount = $this->formatter->currency($discount);
            }
            $base = $this->formatter->currency($result->getBase($ati));
            $taxRates = $this->formatter->rates(...$result->getTaxAdjustments());
            if (0 < $tax = $result->getTax()) {
                $taxAmount = $this->formatter->currency($tax);
            }
            $total = $this->formatter->currency($result->getTotal());
        }

        // TODO Use packaging format
        if ($item->isPrivate()) {
            $quantity = sprintf(
                '%s (x%s)',
                $this->formatter->number($item->getQuantity()),
                $this->formatter->number($item->getParentsQuantity())
            );
        } else {
            $quantity = $this->formatter->number($item->getTotalQuantity());
        }

        $view
            ->setDesignation($item->getDesignation())
            ->setDescription($item->getDescription())
            ->setReference($item->getReference())
            ->setUnit($unit)
            ->setQuantity($quantity)
            ->setGross($gross)
            ->setDiscountRates($discountRates)
            ->setDiscountAmount($discountAmount)
            ->setBase($base)
            ->setTaxRates($taxRates)
            ->setTaxAmount($taxAmount)
            ->setTotal($total);

        if ($item->isPrivate()) {
            $view->setPrivate(true)->addClass('private');
        } else {
            $view->setPrivate(false)->removeClass('private');
        }

        foreach ($this->types as $type) {
            $type->buildItemView($item, $view, $this->options);
        }

        if (!empty($view->getAvailability())) {
            $this->view->vars['show_availability'] = true;
        }

        if ($this->view->vars['show_margin'] && !($item->isCompound() && !$item->hasPrivateChildren())) {
            if (null !== $margin = $item->getMargin()) {
                $view->setMargin(
                    ($margin->isAverage() ? '~' : '') .
                    $this->formatter->percent($margin->getPercent())
                );
            }
        }

        if ($item->hasChildren()) {
            foreach ($item->getChildren() as $child) {
                if (null !== $line = $this->buildSaleItemLineView($child, $level + 1)) {
                    $view->addLine($line);
                }
            }
        }

        return $view;
    }

    /**
     * Builds the sale discount line view.
     *
     * @param Model\SaleAdjustmentInterface $adjustment
     * @param int                           $level
     *
     * @return LineView
     */
    private function buildDiscountLine(Model\SaleAdjustmentInterface $adjustment, $level = 0)
    {
        if (Model\AdjustmentTypes::TYPE_DISCOUNT !== $adjustment->getType()) {
            throw new InvalidArgumentException("Unexpected adjustment type.");
        }

        $lineNumber = $this->lineNumber++;

        $view = new LineView(
            'adjustment_' . ($lineNumber - 1),
            'adjustment_' . $adjustment->getId(),
            $lineNumber,
            $level
        );

        if (empty($designation = $adjustment->getDesignation())) {
            $designation = 'Discount ';
            if ($adjustment->getMode() === Model\AdjustmentModes::MODE_PERCENT) {
                $designation .= $this->formatter->percent($adjustment->getAmount());
            }
        }

        $result = $adjustment->getResult();

        $view
            ->setDesignation($designation)
            ->setBase($this->formatter->currency($result->getBase()))
            ->setTaxAmount($this->formatter->currency($result->getTax()))
            ->setTotal($this->formatter->currency($result->getTotal()));

        foreach ($this->types as $type) {
            $type->buildAdjustmentView($adjustment, $view, $this->options);
        }

        return $view;
    }

    /**
     * Builds the shipment adjustment line view.
     *
     * @param Model\SaleInterface $sale
     */
    private function buildShipmentLine(Model\SaleInterface $sale)
    {
        if (null === $sale->getShipmentMethod() && !$this->options['private']) {
            return;
        }

        $lineNumber = $this->lineNumber++;

        $view = new LineView(
            'shipment',
            'shipment',
            $lineNumber,
            0
        );

        // Method title
        $designation = 'Shipping cost';
        if (null !== $method = $sale->getShipmentMethod()) {
            $designation = $method->getTitle();
        }

        // Total weight
        $designation .= ' (' . $this->formatter->number($sale->getWeightTotal()) . ' kg)';

        $result = $sale->getShipmentResult();

        $view
            ->setDesignation($designation)
            ->setBase($this->formatter->currency($result->getBase()))
            ->setTaxRates($this->formatter->rates(...$result->getTaxAdjustments()))
            ->setTaxAmount($this->formatter->currency($result->getTax()))
            ->setTotal($this->formatter->currency($result->getTotal()));

        foreach ($this->types as $type) {
            $type->buildShipmentView($sale, $view, $this->options);
        }

        $this->view->setShipment($view);
    }

    /**
     * Returns the options resolver.
     *
     * @return OptionsResolver
     */
    private function getOptionsResolver()
    {
        if (null !== $this->optionsResolver) {
            return $this->optionsResolver;
        }

        $resolver = new OptionsResolver();
        $resolver
            ->setDefaults([
                'private'    => false,
                'editable'   => false,
                'taxes_view' => true,
                'ati'        => null,
                'locale'     => \Locale::getDefault(),
                'template'   => function (Options $options) {
                    if (true === $options['editable']) {
                        return $this->editableTemplate;
                    }

                    return $this->defaultTemplate;
                },
            ])
            ->setAllowedTypes('private', 'bool')
            ->setAllowedTypes('editable', 'bool')
            ->setAllowedTypes('taxes_view', 'bool')
            ->setAllowedTypes('ati', ['null', 'bool'])
            ->setAllowedTypes('locale', 'string')
            ->setAllowedTypes('template', ['null', 'string']);

        return $this->optionsResolver = $resolver;
    }
}


<?php

namespace Ekyna\Component\Commerce\Shipment\Resolver;

use Ekyna\Component\Commerce\Common\Model\SaleInterface;
use Ekyna\Component\Commerce\Common\Resolver\StateResolverInterface;
use Ekyna\Component\Commerce\Exception\InvalidArgumentException;
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;
use Ekyna\Component\Commerce\Shipment\Calculator\ShipmentCalculatorInterface;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentStates;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentSubjectInterface;

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

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

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

        // If has at least one preparation shipment
        foreach ($subject->getShipments(true) as $shipment) {
            if ($shipment->getState() === ShipmentStates::STATE_PREPARATION) {
                return $this->setState($subject, ShipmentStates::STATE_PREPARATION);
            }
        }

        // If has at least one pending return
        foreach ($subject->getShipments(false) as $return) {
            if ($return->getState() === ShipmentStates::STATE_PENDING) {
                return $this->setState($subject, ShipmentStates::STATE_PENDING);
            }
        }

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

        $sample = $subject instanceof SaleInterface ? $subject->isSample() : false;

        $partialCount = $shippedCount = $returnedCount = $canceledCount = 0;

        foreach ($quantities as $q) {
            // TODO Use packaging format
            // If shipped greater than zero
            if (0 < $q['shipped']) {
                $sold = $q['total'];
                $shipped = $q['shipped'];
                if (!$sample) {
                    $sold -= $q['credited'];
                    $shipped -= $q['returned'];
                }
                // If sold equals shipped, item is fully shipped
                if (0 === bccomp($sold, $shipped, 3)) {
                    $shippedCount++;

                    // If shipped equals returned, item is fully returned
                    //if ($q['shipped'] == $q['returned']) {
                    if (0 === bccomp($q['shipped'], $q['returned'], 3)) {
                        $returnedCount++;
                    }

                    continue;
                }

                // Item is partially shipped
                $partialCount++;
                continue;
            }

            if (0 === bccomp($q['total'] - $q['credited'], 0, 3)) {
                // Item is canceled
                $canceledCount++;
            }
        }

        // CANCELED If fully canceled
        if ($canceledCount == $itemsCount) {
            return $this->setState($subject, ShipmentStates::STATE_CANCELED);
        }

        // RETURNED If fully returned/canceled
        if ($returnedCount + $canceledCount == $itemsCount) {
            return $this->setState($subject, ShipmentStates::STATE_RETURNED);
        }

        // COMPLETED If fully shipped/canceled
        if ($shippedCount + $canceledCount == $itemsCount) {
            return $this->setState($subject, ShipmentStates::STATE_COMPLETED);
        }

        // PARTIAL If partially shipped
        if (0 < $partialCount || 0 < $shippedCount) {
            return $this->setState($subject, ShipmentStates::STATE_PARTIAL);
        }

        // CANCELED If subject has invoice(s) and is fully credited
        if ($subject instanceof InvoiceSubjectInterface) {
            if ($subject->getInvoiceState() === InvoiceStates::STATE_CREDITED) {
                return $this->setState($subject, ShipmentStates::STATE_CANCELED);
            }
        }

        // 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, ShipmentStates::STATE_CANCELED);
            }
        }

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

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

            return true;
        }

        return false;
    }
}