<commit_before><?php

namespace JMS\Payment\CoreBundle\PluginController;

use JMS\Payment\CoreBundle\Model\CreditInterface;
use JMS\Payment\CoreBundle\Model\FinancialTransactionInterface;
use JMS\Payment\CoreBundle\Model\PaymentInterface;
use JMS\Payment\CoreBundle\Model\PaymentInstructionInterface;
use JMS\Payment\CoreBundle\Plugin\PluginInterface;
use JMS\Payment\CoreBundle\Plugin\QueryablePluginInterface;
use JMS\Payment\CoreBundle\Plugin\Exception\ActionRequiredException as PluginActionRequiredException;
use JMS\Payment\CoreBundle\Plugin\Exception\BlockedException as PluginBlockedException;
use JMS\Payment\CoreBundle\Plugin\Exception\Exception as PluginException;
use JMS\Payment\CoreBundle\Plugin\Exception\FinancialException as PluginFinancialException;
use JMS\Payment\CoreBundle\Plugin\Exception\FunctionNotSupportedException as PluginFunctionNotSupportedException;
use JMS\Payment\CoreBundle\Plugin\Exception\InvalidPaymentInstructionException as PluginInvalidPaymentInstructionException;
use JMS\Payment\CoreBundle\Plugin\Exception\TimeoutException as PluginTimeoutException;
use JMS\Payment\CoreBundle\PluginController\Exception\Exception;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidCreditException;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidPaymentException;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidPaymentInstructionException;
use JMS\Payment\CoreBundle\PluginController\Exception\PluginNotFoundException;
use JMS\Payment\CoreBundle\Util\Number;

/*
 * Copyright 2010 <NAME> <<EMAIL>>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

abstract class PluginController implements PluginControllerInterface
{
    protected $options;
    protected $plugins;

    public function __construct(array $options = array())
    {
        $this->options = $options;
        $this->plugins = array();
    }

    public function addPlugin(PluginInterface $plugin)
    {
        $this->plugins[] = $plugin;
    }

    /**
     * {@inheritDoc}
     */
    public function checkPaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->checkPaymentInstruction($instruction);

            return $this->onSuccessfulPaymentInstructionValidation($instruction);
        } catch (PluginFunctionNotSupportedException $notSupported) {
            return $this->onSuccessfulPaymentInstructionValidation($instruction);
        } catch (PluginInvalidPaymentInstructionException $invalidInstruction) {
            return $this->onUnsuccessfulPaymentInstructionValidation($instruction, $invalidInstruction);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function closePaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $instruction->setState(PaymentInstructionInterface::STATE_CLOSED);
    }

    /**
     * {@inheritDoc}
     */
    public function createPayment($instructionId, $amount)
    {
        $instruction = $this->getPaymentInstruction($instructionId, false);

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction must be in STATE_VALID.');
        }

        // FIXME: Is it practical to check this at all? There can be many payments, credits, etc.
        //        Verify that this is consistent with the checks related to transactions
//        if (Number::compare($amount, $instruction->getAmount()) === 1) {
//            throw new Exception('The Payment\'s target amount must not be greater than the PaymentInstruction\'s amount.');
//        }

        return $this->doCreatePayment($instruction, $amount);
    }

    /**
     * {@inheritDoc}
     */
    public function createPaymentInstruction(PaymentInstructionInterface $paymentInstruction)
    {
        if (PaymentInstructionInterface::STATE_NEW === $paymentInstruction->getState()) {
            $result = $this->validatePaymentInstruction($paymentInstruction);

            if (Result::STATUS_SUCCESS !== $result->getStatus()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction could not be validated.');
            }
        } else if (PaymentInstructionInterface::STATE_VALID !== $paymentInstruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction\'s state must be VALID, or NEW.');
        }

        $this->doCreatePaymentInstruction($paymentInstruction);
    }

    public function getPaymentInstruction($instructionId, $maskSensitiveData = true)
    {
        $paymentInstruction = $this->doGetPaymentInstruction($instructionId);

        if (true === $maskSensitiveData) {
            // FIXME: mask sensitive data
        }

        return $paymentInstruction;
    }

    /**
     * {@inheritDoc}
     */
    public function getRemainingValueOnPaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        if (!$plugin instanceof QueryablePlugin) {
            return null;
        }

        return $plugin->getAvailableBalance($instruction);
    }

    /**
     * {@inheritDoc}
     */
    public function validatePaymentInstruction(PaymentInstructionInterface $paymentInstruction)
    {
        $plugin = $this->getPlugin($paymentInstruction->getPaymentSystemName());

        try {
            $plugin->validatePaymentInstruction($paymentInstruction);

            return $this->onSuccessfulPaymentInstructionValidation($paymentInstruction);
        } catch (PluginFunctionNotSupportedException $notSupported) {
            return $this->checkPaymentInstruction($paymentInstruction);
        } catch (PluginInvalidPaymentInstructionException $invalid) {
            return $this->onUnsuccessfulPaymentInstructionValidation($paymentInstruction, $invalid);
        }
    }

    protected function buildFinancialTransactionResult(FinancialTransactionInterface $transaction, $status, $reasonCode)
    {
        $class = &$this->options['result_class'];

        return new $class($transaction, $status, $reasonCode);
    }

    protected function buildPaymentInstructionResult(PaymentInstructionInterface $instruction, $status, $reasonCode)
    {
        $class = &$this->options['result_class'];

        return new $class($instruction, $status, $reasonCode);
    }

    abstract protected function buildCredit(PaymentInstructionInterface $paymentInstruction, $amount);

    abstract protected function buildFinancialTransaction();

    protected function doApprove(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction\'s state must be STATE_VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_NEW === $paymentState) {
            if (Number::compare($payment->getTargetAmount(), $amount) < 0) {
                throw new Exception('The Payment\'s target amount is less than the requested amount.');
            }

            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction can only ever have one pending transaction.');
            }

            $retry = false;

            $transaction = $this->buildFinancialTransaction();
            $transaction->setPayment($payment);
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_APPROVE);
            $transaction->setRequestedAmount($amount);

            $payment->setState(PaymentInterface::STATE_APPROVING);
            $payment->setApprovingAmount($amount);
            $instruction->setApprovingAmount($instruction->getApprovingAmount() + $amount);
        } else if (PaymentInterface::STATE_APPROVING === $paymentState) {
            if (Number::compare($payment->getTargetAmount(), $amount) !== 0) {
                throw new Exception('The Payment\'s target amount must equal the requested amount in a retry transaction.');
            }

            $transaction = $payment->getApproveTransaction();
            $retry = true;
        } else {
            throw new InvalidPaymentException('The Payment\'s state must be STATE_NEW, or STATE_APPROVING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->approve($transaction, $retry);

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $payment->setState(PaymentInterface::STATE_APPROVED);
                $payment->setApprovingAmount(0.0);
                $payment->setApprovedAmount($transaction->getProcessedAmount());
                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setApprovedAmount($instruction->getApprovedAmount() + $transaction->getProcessedAmount());
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $payment->setState(PaymentInterface::STATE_FAILED);
                $payment->setApprovingAmount(0.0);
                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $payment->setState(PaymentInterface::STATE_FAILED);
            $payment->setApprovingAmount(0.0);
            $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function doApproveAndDeposit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_NEW === $paymentState) {
            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('PaymentInstruction can only ever have one pending transaction.');
            }

            if (1 === Number::compare($amount, $payment->getTargetAmount())) {
                throw new \InvalidArgumentException('$amount must not be greater than Payment\'s target amount.');
            }

            $transaction = $this->buildFinancialTransaction();
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_APPROVE_AND_DEPOSIT);
            $transaction->setPayment($payment);
            $transaction->setRequestedAmount($amount);
            $payment->addTransaction($transaction);

            $payment->setApprovingAmount($amount);
            $payment->setDepositingAmount($amount);
            $payment->setState(PaymentInterface::STATE_APPROVING);

            $instruction->setApprovingAmount($instruction->getApprovingAmount() + $amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() + $amount);

            $retry = false;
        } else if (PaymentInterface::STATE_APPROVING === $paymentState) {
            if (0 !== Number::compare($amount, $payment->getApprovingAmount())) {
                throw new \InvalidArgumentException('$amount must be equal to Payment\'s approving amount.');
            }

            if (0 !== Number::compare($amount, $payment->getDepositingAmount())) {
                throw new \InvalidArgumentException('$amount must be equal to Payment\'s depositing amount.');
            }

            $transaction = $payment->getApproveTransaction();

            $retry = true;
        } else {
            throw new InvalidPaymentException('Payment\'s state must e NEW, or APPROVING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->approveAndDeposit($transaction, $retry);

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);
                $processedAmount = $transaction->getProcessedAmount();

                $payment->setState(PaymentInterface::STATE_APPROVED);
                $payment->setApprovingAmount(0.0);
                $payment->setDepositingAmount(0.0);
                $payment->setApprovedAmount($processedAmount);
                $payment->setDepositedAmount($processedAmount);

                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);
                $instruction->setApprovedAmount($instruction->getApprovedAmount() + $processedAmount);
                $instruction->setDepositedAmount($instruction->getDepositedAmount() + $processedAmount);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

                $payment->setState(PaymentInterface::STATE_FAILED);
                $payment->setApprovingAmount(0.0);
                $payment->setDepositingAmount(0.0);

                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

            $payment->setState(PaymentInterface::STATE_FAILED);
            $payment->setApprovingAmount(0.0);
            $payment->setDepositingAmount(0.0);

            $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    protected function doCreateDependentCredit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
            throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
        }

        $credit = $this->buildCredit($instruction, $amount);
        $credit->setPayment($payment);

        return $credit;
    }

    protected function doCreateIndependentCredit(PaymentInstructionInterface $instruction, $amount)
    {
        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        return $this->buildCredit($instruction, $amount);
    }

    abstract protected function doCreatePayment(PaymentInstructionInterface $instruction, $amount);

    abstract protected function doCreatePaymentInstruction(PaymentInstructionInterface $instruction);

    protected function doCredit(CreditInterface $credit, $amount)
    {
        $instruction = $credit->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction must be in STATE_VALID.');
        }

        $creditState = $credit->getState();
        if (CreditInterface::STATE_NEW === $creditState) {
            if (1 === Number::compare($amount, $max = $instruction->getDepositedAmount() - $instruction->getReversingDepositedAmount() - $instruction->getCreditingAmount() - $instruction->getCreditedAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (PaymentInstruction restriction).', $max));
            }

            if (1 === Number::compare($amount, $credit->getTargetAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Credit restriction).', $credit->getTargetAmount()));
            }

            if (false === $credit->isIndependent()) {
                $payment = $credit->getPayment();
                $paymentState = $payment->getState();
                if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
                    throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
                }

                if (1 === Number::compare($amount, $max = $payment->getDepositedAmount() - $payment->getReversingDepositedAmount() - $payment->getCreditingAmount() - $payment->getCreditedAmount())) {
                    throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Payment restriction).', $max));
                }
            }

            $transaction = $this->buildFinancialTransaction();
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_CREDIT);
            $transaction->setRequestedAmount($amount);
            $credit->addTransaction($transaction);

            $credit->setCreditingAmount($amount);
            $instruction->setCreditingAmount($instruction->getCreditingAmount() + $amount);

            if (false === $credit->isIndependent()) {
                $payment->setCreditingAmount($payment->getCreditingAmount() + $amount);
            }

            $retry = false;
        } else if (CreditInterface::STATE_CREDITING === $creditState) {
            if (1 === Number::compare($amount, $instruction->getCreditingAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (PaymentInstruction restriction).', $instruction->getCreditingAmount()));
            }
            if (0 !== Number::compare($amount, $credit->getCreditingAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount must be equal to %.2f (Credit restriction).', $credit->getCreditingAmount()));
            }

            if (false === $credit->isIndependent()) {
                $payment = $credit->getPayment();
                $paymentState = $payment->getState();
                if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
                    throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
                }

                if (1 === Number::compare($amount, $payment->getCreditingAmount())) {
                    throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Payment restriction).', $payment->getCreditingAmount()));
                }
            }

            $transaction = $credit->getCreditTransaction();

            $retry = true;
        } else {
            throw new InvalidCreditException('Credit\'s state must be NEW, or CREDITING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->credit($transaction, $retry);
            $processedAmount = $transaction->getProcessedAmount();

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);
                $credit->setState(CreditInterface::STATE_CREDITED);

                $credit->setCreditingAmount(0.0);
                $credit->setCreditedAmount($processedAmount);
                $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);
                $instruction->setCreditedAmount($instruction->getCreditedAmount() + $processedAmount);

                if (false === $credit->isIndependent()) {
                    $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
                    $payment->setCreditedAmount($payment->getCreditedAmount() + $processedAmount);
                }

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);
                $credit->setState(CreditInterface::STATE_FAILED);

                $credit->setCreditingAmount(0.0);
                $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);

                if (false === $credit->isIndependent()) {
                    $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
                }

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);
            $credit->setState(CreditInterface::STATE_FAILED);

            $credit->setCreditingAmount(0.0);
            $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);

            if (false === $credit->isIndependent()) {
                $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
            }

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    protected function doDeposit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction must be in STATE_VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_APPROVED === $paymentState) {
            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction can only have one pending transaction at a time.');
            }

            if (Number::compare($amount, $payment->getApprovedAmount()) === 1) {
                throw new Exception('The amount cannot be greater than the approved amount of the Payment.');
            }

            $retry = false;

            $transaction = $this->createFinancialTransaction($payment);
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_DEPOSIT);
            $transaction->setRequestedAmount($amount);

            $payment->setState(PaymentInterface::STATE_DEPOSITING);
            $payment->setDepositingAmount($amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() + $amount);
        } else if (PaymentInstructionInterface::STATE_DEPOSITING === $paymentState) {
            $transaction = $instructin->getPendingTransaction();
            if (null === $transaction) {
                if (Number::compare($amount, $payment->getApprovedAmount() - $payment->getDepositedAmount()) === 1) {
                    throw new Exception('The amount cannot be greater than the approved amount minus the already deposited amount.');<commit_msg>fixed another typo<commit_after><?php

namespace JMS\Payment\CoreBundle\PluginController;

use JMS\Payment\CoreBundle\Model\CreditInterface;
use JMS\Payment\CoreBundle\Model\FinancialTransactionInterface;
use JMS\Payment\CoreBundle\Model\PaymentInterface;
use JMS\Payment\CoreBundle\Model\PaymentInstructionInterface;
use JMS\Payment\CoreBundle\Plugin\PluginInterface;
use JMS\Payment\CoreBundle\Plugin\QueryablePluginInterface;
use JMS\Payment\CoreBundle\Plugin\Exception\ActionRequiredException as PluginActionRequiredException;
use JMS\Payment\CoreBundle\Plugin\Exception\BlockedException as PluginBlockedException;
use JMS\Payment\CoreBundle\Plugin\Exception\Exception as PluginException;
use JMS\Payment\CoreBundle\Plugin\Exception\FinancialException as PluginFinancialException;
use JMS\Payment\CoreBundle\Plugin\Exception\FunctionNotSupportedException as PluginFunctionNotSupportedException;
use JMS\Payment\CoreBundle\Plugin\Exception\InvalidPaymentInstructionException as PluginInvalidPaymentInstructionException;
use JMS\Payment\CoreBundle\Plugin\Exception\TimeoutException as PluginTimeoutException;
use JMS\Payment\CoreBundle\PluginController\Exception\Exception;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidCreditException;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidPaymentException;
use JMS\Payment\CoreBundle\PluginController\Exception\InvalidPaymentInstructionException;
use JMS\Payment\CoreBundle\PluginController\Exception\PluginNotFoundException;
use JMS\Payment\CoreBundle\Util\Number;

/*
 * Copyright 2010 <NAME> <<EMAIL>>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

abstract class PluginController implements PluginControllerInterface
{
    protected $options;
    protected $plugins;

    public function __construct(array $options = array())
    {
        $this->options = $options;
        $this->plugins = array();
    }

    public function addPlugin(PluginInterface $plugin)
    {
        $this->plugins[] = $plugin;
    }

    /**
     * {@inheritDoc}
     */
    public function checkPaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->checkPaymentInstruction($instruction);

            return $this->onSuccessfulPaymentInstructionValidation($instruction);
        } catch (PluginFunctionNotSupportedException $notSupported) {
            return $this->onSuccessfulPaymentInstructionValidation($instruction);
        } catch (PluginInvalidPaymentInstructionException $invalidInstruction) {
            return $this->onUnsuccessfulPaymentInstructionValidation($instruction, $invalidInstruction);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function closePaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $instruction->setState(PaymentInstructionInterface::STATE_CLOSED);
    }

    /**
     * {@inheritDoc}
     */
    public function createPayment($instructionId, $amount)
    {
        $instruction = $this->getPaymentInstruction($instructionId, false);

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction must be in STATE_VALID.');
        }

        // FIXME: Is it practical to check this at all? There can be many payments, credits, etc.
        //        Verify that this is consistent with the checks related to transactions
//        if (Number::compare($amount, $instruction->getAmount()) === 1) {
//            throw new Exception('The Payment\'s target amount must not be greater than the PaymentInstruction\'s amount.');
//        }

        return $this->doCreatePayment($instruction, $amount);
    }

    /**
     * {@inheritDoc}
     */
    public function createPaymentInstruction(PaymentInstructionInterface $paymentInstruction)
    {
        if (PaymentInstructionInterface::STATE_NEW === $paymentInstruction->getState()) {
            $result = $this->validatePaymentInstruction($paymentInstruction);

            if (Result::STATUS_SUCCESS !== $result->getStatus()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction could not be validated.');
            }
        } else if (PaymentInstructionInterface::STATE_VALID !== $paymentInstruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction\'s state must be VALID, or NEW.');
        }

        $this->doCreatePaymentInstruction($paymentInstruction);
    }

    public function getPaymentInstruction($instructionId, $maskSensitiveData = true)
    {
        $paymentInstruction = $this->doGetPaymentInstruction($instructionId);

        if (true === $maskSensitiveData) {
            // FIXME: mask sensitive data
        }

        return $paymentInstruction;
    }

    /**
     * {@inheritDoc}
     */
    public function getRemainingValueOnPaymentInstruction(PaymentInstructionInterface $instruction)
    {
        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        if (!$plugin instanceof QueryablePlugin) {
            return null;
        }

        return $plugin->getAvailableBalance($instruction);
    }

    /**
     * {@inheritDoc}
     */
    public function validatePaymentInstruction(PaymentInstructionInterface $paymentInstruction)
    {
        $plugin = $this->getPlugin($paymentInstruction->getPaymentSystemName());

        try {
            $plugin->validatePaymentInstruction($paymentInstruction);

            return $this->onSuccessfulPaymentInstructionValidation($paymentInstruction);
        } catch (PluginFunctionNotSupportedException $notSupported) {
            return $this->checkPaymentInstruction($paymentInstruction);
        } catch (PluginInvalidPaymentInstructionException $invalid) {
            return $this->onUnsuccessfulPaymentInstructionValidation($paymentInstruction, $invalid);
        }
    }

    protected function buildFinancialTransactionResult(FinancialTransactionInterface $transaction, $status, $reasonCode)
    {
        $class = &$this->options['result_class'];

        return new $class($transaction, $status, $reasonCode);
    }

    protected function buildPaymentInstructionResult(PaymentInstructionInterface $instruction, $status, $reasonCode)
    {
        $class = &$this->options['result_class'];

        return new $class($instruction, $status, $reasonCode);
    }

    abstract protected function buildCredit(PaymentInstructionInterface $paymentInstruction, $amount);

    abstract protected function buildFinancialTransaction();

    protected function doApprove(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction\'s state must be STATE_VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_NEW === $paymentState) {
            if (Number::compare($payment->getTargetAmount(), $amount) < 0) {
                throw new Exception('The Payment\'s target amount is less than the requested amount.');
            }

            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction can only ever have one pending transaction.');
            }

            $retry = false;

            $transaction = $this->buildFinancialTransaction();
            $transaction->setPayment($payment);
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_APPROVE);
            $transaction->setRequestedAmount($amount);

            $payment->setState(PaymentInterface::STATE_APPROVING);
            $payment->setApprovingAmount($amount);
            $instruction->setApprovingAmount($instruction->getApprovingAmount() + $amount);
        } else if (PaymentInterface::STATE_APPROVING === $paymentState) {
            if (Number::compare($payment->getTargetAmount(), $amount) !== 0) {
                throw new Exception('The Payment\'s target amount must equal the requested amount in a retry transaction.');
            }

            $transaction = $payment->getApproveTransaction();
            $retry = true;
        } else {
            throw new InvalidPaymentException('The Payment\'s state must be STATE_NEW, or STATE_APPROVING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->approve($transaction, $retry);

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $payment->setState(PaymentInterface::STATE_APPROVED);
                $payment->setApprovingAmount(0.0);
                $payment->setApprovedAmount($transaction->getProcessedAmount());
                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setApprovedAmount($instruction->getApprovedAmount() + $transaction->getProcessedAmount());
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $payment->setState(PaymentInterface::STATE_FAILED);
                $payment->setApprovingAmount(0.0);
                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $payment->setState(PaymentInterface::STATE_FAILED);
            $payment->setApprovingAmount(0.0);
            $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function doApproveAndDeposit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_NEW === $paymentState) {
            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('PaymentInstruction can only ever have one pending transaction.');
            }

            if (1 === Number::compare($amount, $payment->getTargetAmount())) {
                throw new \InvalidArgumentException('$amount must not be greater than Payment\'s target amount.');
            }

            $transaction = $this->buildFinancialTransaction();
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_APPROVE_AND_DEPOSIT);
            $transaction->setPayment($payment);
            $transaction->setRequestedAmount($amount);
            $payment->addTransaction($transaction);

            $payment->setApprovingAmount($amount);
            $payment->setDepositingAmount($amount);
            $payment->setState(PaymentInterface::STATE_APPROVING);

            $instruction->setApprovingAmount($instruction->getApprovingAmount() + $amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() + $amount);

            $retry = false;
        } else if (PaymentInterface::STATE_APPROVING === $paymentState) {
            if (0 !== Number::compare($amount, $payment->getApprovingAmount())) {
                throw new \InvalidArgumentException('$amount must be equal to Payment\'s approving amount.');
            }

            if (0 !== Number::compare($amount, $payment->getDepositingAmount())) {
                throw new \InvalidArgumentException('$amount must be equal to Payment\'s depositing amount.');
            }

            $transaction = $payment->getApproveTransaction();

            $retry = true;
        } else {
            throw new InvalidPaymentException('Payment\'s state must e NEW, or APPROVING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->approveAndDeposit($transaction, $retry);

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);
                $processedAmount = $transaction->getProcessedAmount();

                $payment->setState(PaymentInterface::STATE_APPROVED);
                $payment->setApprovingAmount(0.0);
                $payment->setDepositingAmount(0.0);
                $payment->setApprovedAmount($processedAmount);
                $payment->setDepositedAmount($processedAmount);

                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);
                $instruction->setApprovedAmount($instruction->getApprovedAmount() + $processedAmount);
                $instruction->setDepositedAmount($instruction->getDepositedAmount() + $processedAmount);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

                $payment->setState(PaymentInterface::STATE_FAILED);
                $payment->setApprovingAmount(0.0);
                $payment->setDepositingAmount(0.0);

                $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
                $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);

            $payment->setState(PaymentInterface::STATE_FAILED);
            $payment->setApprovingAmount(0.0);
            $payment->setDepositingAmount(0.0);

            $instruction->setApprovingAmount($instruction->getApprovingAmount() - $amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() - $amount);

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    protected function doCreateDependentCredit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
            throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
        }

        $credit = $this->buildCredit($instruction, $amount);
        $credit->setPayment($payment);

        return $credit;
    }

    protected function doCreateIndependentCredit(PaymentInstructionInterface $instruction, $amount)
    {
        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction\'s state must be VALID.');
        }

        return $this->buildCredit($instruction, $amount);
    }

    abstract protected function doCreatePayment(PaymentInstructionInterface $instruction, $amount);

    abstract protected function doCreatePaymentInstruction(PaymentInstructionInterface $instruction);

    protected function doCredit(CreditInterface $credit, $amount)
    {
        $instruction = $credit->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('PaymentInstruction must be in STATE_VALID.');
        }

        $creditState = $credit->getState();
        if (CreditInterface::STATE_NEW === $creditState) {
            if (1 === Number::compare($amount, $max = $instruction->getDepositedAmount() - $instruction->getReversingDepositedAmount() - $instruction->getCreditingAmount() - $instruction->getCreditedAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (PaymentInstruction restriction).', $max));
            }

            if (1 === Number::compare($amount, $credit->getTargetAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Credit restriction).', $credit->getTargetAmount()));
            }

            if (false === $credit->isIndependent()) {
                $payment = $credit->getPayment();
                $paymentState = $payment->getState();
                if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
                    throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
                }

                if (1 === Number::compare($amount, $max = $payment->getDepositedAmount() - $payment->getReversingDepositedAmount() - $payment->getCreditingAmount() - $payment->getCreditedAmount())) {
                    throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Payment restriction).', $max));
                }
            }

            $transaction = $this->buildFinancialTransaction();
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_CREDIT);
            $transaction->setRequestedAmount($amount);
            $credit->addTransaction($transaction);

            $credit->setCreditingAmount($amount);
            $instruction->setCreditingAmount($instruction->getCreditingAmount() + $amount);

            if (false === $credit->isIndependent()) {
                $payment->setCreditingAmount($payment->getCreditingAmount() + $amount);
            }

            $retry = false;
        } else if (CreditInterface::STATE_CREDITING === $creditState) {
            if (1 === Number::compare($amount, $instruction->getCreditingAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (PaymentInstruction restriction).', $instruction->getCreditingAmount()));
            }
            if (0 !== Number::compare($amount, $credit->getCreditingAmount())) {
                throw new \InvalidArgumentException(sprintf('$amount must be equal to %.2f (Credit restriction).', $credit->getCreditingAmount()));
            }

            if (false === $credit->isIndependent()) {
                $payment = $credit->getPayment();
                $paymentState = $payment->getState();
                if (PaymentInterface::STATE_APPROVED !== $paymentState && PaymentInterface::STATE_EXPIRED !== $paymentState) {
                    throw new InvalidPaymentException('Payment\'s state must be APPROVED, or EXPIRED.');
                }

                if (1 === Number::compare($amount, $payment->getCreditingAmount())) {
                    throw new \InvalidArgumentException(sprintf('$amount cannot be greater than %.2f (Payment restriction).', $payment->getCreditingAmount()));
                }
            }

            $transaction = $credit->getCreditTransaction();

            $retry = true;
        } else {
            throw new InvalidCreditException('Credit\'s state must be NEW, or CREDITING.');
        }

        $plugin = $this->getPlugin($instruction->getPaymentSystemName());

        try {
            $plugin->credit($transaction, $retry);
            $processedAmount = $transaction->getProcessedAmount();

            if (PluginInterface::RESPONSE_CODE_SUCCESS === $transaction->getResponseCode()) {
                $transaction->setState(FinancialTransactionInterface::STATE_SUCCESS);
                $credit->setState(CreditInterface::STATE_CREDITED);

                $credit->setCreditingAmount(0.0);
                $credit->setCreditedAmount($processedAmount);
                $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);
                $instruction->setCreditedAmount($instruction->getCreditedAmount() + $processedAmount);

                if (false === $credit->isIndependent()) {
                    $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
                    $payment->setCreditedAmount($payment->getCreditedAmount() + $processedAmount);
                }

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_SUCCESS, PluginInterface::REASON_CODE_SUCCESS);
            } else {
                $transaction->setState(FinancialTransactionInterface::STATE_FAILED);
                $credit->setState(CreditInterface::STATE_FAILED);

                $credit->setCreditingAmount(0.0);
                $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);

                if (false === $credit->isIndependent()) {
                    $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
                }

                return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
            }
        } catch (PluginFinancialException $ex) {
            $transaction->setState(FinancialTransactionInterface::STATE_FAILED);
            $credit->setState(CreditInterface::STATE_FAILED);

            $credit->setCreditingAmount(0.0);
            $instruction->setCreditingAmount($instruction->getCreditingAmount() - $amount);

            if (false === $credit->isIndependent()) {
                $payment->setCreditingAmount($payment->getCreditingAmount() - $amount);
            }

            return $this->buildFinancialTransactionResult($transaction, Result::STATUS_FAILED, $transaction->getReasonCode());
        } catch (PluginBlockedException $blocked) {
            $transaction->setState(FinancialTransactionInterface::STATE_PENDING);

            if ($blocked instanceof PluginTimeoutException) {
                $reasonCode = PluginInterface::REASON_CODE_TIMEOUT;
            } else if ($blocked instanceof PluginActionRequiredException) {
                $reasonCode = PluginInterface::REASON_CODE_ACTION_REQUIRED;
            } else if (null === $reasonCode = $transaction->getReasonCode()) {
                $reasonCode = PluginInterface::REASON_CODE_BLOCKED;
            }
            $transaction->setReasonCode($reasonCode);
            $transaction->setResponseCode(PluginInterface::RESPONSE_CODE_PENDING);

            $result = $this->buildFinancialTransactionResult($transaction, Result::STATUS_PENDING, $reasonCode);
            $result->setPluginException($blocked);
            $result->setRecoverable();

            return $result;
        }
    }

    protected function doDeposit(PaymentInterface $payment, $amount)
    {
        $instruction = $payment->getPaymentInstruction();

        if (PaymentInstructionInterface::STATE_VALID !== $instruction->getState()) {
            throw new InvalidPaymentInstructionException('The PaymentInstruction must be in STATE_VALID.');
        }

        $paymentState = $payment->getState();
        if (PaymentInterface::STATE_APPROVED === $paymentState) {
            if ($instruction->hasPendingTransaction()) {
                throw new InvalidPaymentInstructionException('The PaymentInstruction can only have one pending transaction at a time.');
            }

            if (Number::compare($amount, $payment->getApprovedAmount()) === 1) {
                throw new Exception('The amount cannot be greater than the approved amount of the Payment.');
            }

            $retry = false;

            $transaction = $this->createFinancialTransaction($payment);
            $transaction->setTransactionType(FinancialTransactionInterface::TRANSACTION_TYPE_DEPOSIT);
            $transaction->setRequestedAmount($amount);

            $payment->setState(PaymentInterface::STATE_DEPOSITING);
            $payment->setDepositingAmount($amount);
            $instruction->setDepositingAmount($instruction->getDepositingAmount() + $amount);
        } else if (PaymentInterface::STATE_DEPOSITING === $paymentState) {
            $transaction = $instructin->getPendingTransaction();
            if (null === $transaction) {
                if (Number::compare($amount, $payment->getApprovedAmount() - $payment->getDepositedAmount()) === 1) {
                    throw new Exception('The amount cannot be greater than the approved amount minus the already deposited amount.');
{'fraction_non_alphanumeric': 0.11403320452268499, 'fraction_numerical': 0.0018605982539001001, 'mean_word_length': 3.1152175513509532, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 515, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '113135', 'n_tokens_mistral': 13585, 'n_tokens_neox': 13071, 'n_words': 2441}