<commit_before><?php
/*
 * Fusio
 * A web-application to create dynamically RESTful APIs
 *
 * Copyright (C) 2015-2018 <NAME> <<EMAIL>>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

namespace Fusio\Impl\Service;

use Fusio\Engine\ConnectorInterface;
use Fusio\Engine\Model\Product;
use Fusio\Engine\Model\ProductInterface;
use Fusio\Engine\Model\Transaction as TransactionModel;
use Fusio\Engine\Model\TransactionInterface;
use Fusio\Engine\Payment\ProviderInterface;
use Fusio\Engine\Payment\RedirectUrls;
use Fusio\Impl\Authorization\UserContext;
use Fusio\Impl\Event\Transaction\ExecutedEvent;
use Fusio\Impl\Event\Transaction\PreparedEvent;
use Fusio\Impl\Event\TransactionEvents;
use Fusio\Impl\Provider\ProviderFactory;
use Fusio\Impl\Service\Plan\Payer;
use Fusio\Impl\Table;
use PSX\Framework\Config\Config;
use PSX\Framework\Util\Uuid;
use PSX\Http\Exception as StatusCode;
use PSX\Sql\Condition;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Transaction
 *
 * @author  <NAME> <<EMAIL>>
 * @license http://www.gnu.org/licenses/agpl-3.0
 * @link    http://fusio-project.org
 */
class Transaction
{
    /**
     * @var \Fusio\Engine\ConnectorInterface
     */
    protected $connector;

    /**
     * @var \Fusio\Impl\Service\Plan\Payer
     */
    protected $payerService;

    /**
     * @var \Fusio\Impl\Provider\ProviderFactory
     */
    protected $providerFactory;

    /**
     * @var \PSX\Framework\Config\Config $config
     */
    protected $config;

    /**
     * @var \Fusio\Impl\Table\Plan
     */
    protected $planTable;

    /**
     * @var \Fusio\Impl\Table\Transaction
     */
    protected $transactionTable;

    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;

    /**
     * @param \Fusio\Engine\ConnectorInterface $connector
     * @param \Fusio\Impl\Service\Plan\Payer $payerService
     * @param \Fusio\Impl\Provider\ProviderFactory $providerFactory
     * @param \PSX\Framework\Config\Config $config
     * @param \Fusio\Impl\Table\Plan $planTable
     * @param \Fusio\Impl\Table\Transaction $transactionTable
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
     */
    public function __construct(ConnectorInterface $connector, Payer $payerService, ProviderFactory $providerFactory, Config $config, Table\Plan $planTable, Table\Transaction $transactionTable, EventDispatcherInterface $eventDispatcher)
    {
        $this->connector = $connector;
        $this->payerService = $payerService;
        $this->providerFactory = $providerFactory;
        $this->config = $config;
        $this->planTable = $planTable;
        $this->transactionTable = $transactionTable;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * @param string $name
     * @param integer $planId
     * @param string $returnUrl
     * @param \Fusio\Impl\Authorization\UserContext $context
     * @return string
     */
    public function prepare($name, $planId, $returnUrl, UserContext $context)
    {
        $provider = $this->providerFactory->factory($name);

        if (!$provider instanceof ProviderInterface) {
            throw new StatusCode\BadRequestException('Provider is not available');
        }

        $product    = $this->createProduct($planId);
        $connection = $this->connector->getConnection($name);

        // validate return url
        if (empty($returnUrl) || !filter_var($returnUrl, FILTER_VALIDATE_URL)) {
            throw new StatusCode\BadRequestException('Invalid return url');
        }

        // create transaction
        $transaction = new TransactionModel();
        $transaction->setPlanId($product->getId());
        $transaction->setUserId($context->getUserId());
        $transaction->setAppId($context->getAppId());
        $transaction->setProvider($name);
        $transaction->setTransactionId(Uuid::pseudoRandom());
        $transaction->setReturnUrl($returnUrl);

        $this->transactionTable->beginTransaction();

        try {
            // create transaction
            $this->transactionTable->create([
                'plan_id' => $transaction->getPlanId(),
                'user_id' => $transaction->getUserId(),
                'app_id' => $transaction->getAppId(),
                'status' => TransactionModel::STATUS_CREATED,
                'provider' => $transaction->getProvider(),
                'transaction_id' => $transaction->getTransactionId(),
                'amount' => $product->getPrice(),
                'return_url' => $transaction->getReturnUrl(),
                'insert_date' => new \DateTime(),
            ]);

            // set transaction id
            $transaction->setId($this->transactionTable->getLastInsertId());

            // prepare payment
            $approvalUrl = $provider->prepare(
                $connection,
                $product,
                $transaction,
                $this->buildRedirectUrls($transaction)
            );

            // update transaction
            $this->updateTransaction($product, $transaction);

            // trigger event
            $this->eventDispatcher->dispatch(TransactionEvents::PREPARE, new PreparedEvent($transaction));

            $this->transactionTable->commit();

            return $approvalUrl;
        } catch (\Throwable $e) {
            $this->transactionTable->rollBack();

            throw $e;
        }
    }

    /**
     * @param integer $transactionId
     * @param array $parameters
     * @return string
     */
    public function execute($transactionId, array $parameters)
    {
        $transaction = $this->createTransaction($transactionId);
        $provider    = $this->providerFactory->factory($transaction->getProvider());

        if (!$provider instanceof ProviderInterface) {
            throw new StatusCode\BadRequestException('Provider is not available');
        }

        $connection = $this->connector->getConnection($transaction->getProvider());

        $this->transactionTable->beginTransaction();

        try {
            // create product
            $product = $this->createProduct($transaction->getPlanId());

            // execute transaction
            $provider->execute($connection, $product, $transaction, $parameters);

            // update transaction
            $this->updateTransaction($product, $transaction);

            // trigger event
            $this->eventDispatcher->dispatch(TransactionEvents::EXECUTE, new ExecutedEvent($transaction));

            $this->transactionTable->commit();

            return $this->buildReturnUrl($transaction);
        } catch (\Throwable $e) {
            $this->transactionTable->rollBack();

            throw $e;
        }
    }

    /**
     * @param integer $planId
     * @return \Fusio\Engine\Model\ProductInterface
     */
    private function createProduct($planId)
    {
        $plan = $this->planTable->get($planId);

        if (empty($plan)) {
            throw new StatusCode\BadRequestException('Invalid plan id');
        }

        if ($plan['status'] != Table\Plan::STATUS_ACTIVE) {
            throw new StatusCode\BadRequestException('Invalid plan status');
        }

        $product = new Product();
        $product->setId($plan['id']);
        $product->setName($plan['name']);
        $product->setPrice($plan['price']);
        $product->setPoints($plan['points']);

        return $product;
    }

    /**
     * @param integer $transactionId
     * @return \Fusio\Engine\Model\Transaction
     */
    private function createTransaction($transactionId)
    {
        $condition = new Condition();
        $condition->equals('transaction_id', $transactionId);

        $result = $this->transactionTable->getOneBy($condition);

        if (empty($result)) {
            throw new StatusCode\BadRequestException('Invalid transaction id');
        }

        if ($result['status'] == TransactionInterface::STATUS_APPROVED) {
            throw new StatusCode\BadRequestException('Transaction is already approved');
        }

        $transaction = new TransactionModel();
        $transaction->setId($result['id']);
        $transaction->setPlanId($result['plan_id']);
        $transaction->setUserId($result['user_id']);
        $transaction->setAppId($result['app_id']);
        $transaction->setStatus($result['status']);
        $transaction->setProvider($result['provider']);
        $transaction->setTransactionId($result['transaction_id']);
        $transaction->setAmount($result['amount']);
        $transaction->setReturnUrl($result['return_url']);

        $updateDate = $result['update_date'];
        if (!empty($updateDate)) {
            $transaction->setUpdateDate($updateDate);
        }

        $transaction->setCreateDate($result['insert_date']);

        return $transaction;
    }

    /**
     * @param \Fusio\Engine\Model\ProductInterface $product
     * @param \Fusio\Engine\Model\TransactionInterface $transaction
     */
    private function updateTransaction(ProductInterface $product, TransactionInterface $transaction)
    {
        // update transaction
        $this->transactionTable->update([
            'id' => $transaction->getId(),
            'status' => $transaction->getStatus(),
            'remote_id' => $transaction->getRemoteId(),
            'update_date' => new \DateTime(),
        ]);

        // if approved add points to user
        if ($transaction->getStatus() == TransactionInterface::STATUS_APPROVED) {
            $this->payerService->credit($transaction->getUserId(), $product->getPoints());
        }
    }

    /**
     * @param \Fusio\Engine\Model\TransactionInterface $transaction
     * @return \Fusio\Engine\Payment\RedirectUrls
     */
    private function buildRedirectUrls(TransactionInterface $transaction)
    {
        $baseUrl = $this->config->get('psx_url') . '/' . $this->config->get('psx_dispatch');

        $redirectUrls = new RedirectUrls();
        $redirectUrls->setReturnUrl($baseUrl . 'consumer/plan/payment/execute/' . $transaction->getTransactionId());<commit_msg>pass user context to credit points<commit_after><?php
/*
 * Fusio
 * A web-application to create dynamically RESTful APIs
 *
 * Copyright (C) 2015-2018 <NAME> <<EMAIL>>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

namespace Fusio\Impl\Service;

use Fusio\Engine\ConnectorInterface;
use Fusio\Engine\Model\Product;
use Fusio\Engine\Model\ProductInterface;
use Fusio\Engine\Model\Transaction as TransactionModel;
use Fusio\Engine\Model\TransactionInterface;
use Fusio\Engine\Payment\ProviderInterface;
use Fusio\Engine\Payment\RedirectUrls;
use Fusio\Impl\Authorization\UserContext;
use Fusio\Impl\Event\Transaction\ExecutedEvent;
use Fusio\Impl\Event\Transaction\PreparedEvent;
use Fusio\Impl\Event\TransactionEvents;
use Fusio\Impl\Provider\ProviderFactory;
use Fusio\Impl\Service\Plan\Payer;
use Fusio\Impl\Table;
use PSX\Framework\Config\Config;
use PSX\Framework\Util\Uuid;
use PSX\Http\Exception as StatusCode;
use PSX\Sql\Condition;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Transaction
 *
 * @author  <NAME> <<EMAIL>>
 * @license http://www.gnu.org/licenses/agpl-3.0
 * @link    http://fusio-project.org
 */
class Transaction
{
    /**
     * @var \Fusio\Engine\ConnectorInterface
     */
    protected $connector;

    /**
     * @var \Fusio\Impl\Service\Plan\Payer
     */
    protected $payerService;

    /**
     * @var \Fusio\Impl\Provider\ProviderFactory
     */
    protected $providerFactory;

    /**
     * @var \PSX\Framework\Config\Config $config
     */
    protected $config;

    /**
     * @var \Fusio\Impl\Table\Plan
     */
    protected $planTable;

    /**
     * @var \Fusio\Impl\Table\Transaction
     */
    protected $transactionTable;

    /**
     * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
     */
    protected $eventDispatcher;

    /**
     * @param \Fusio\Engine\ConnectorInterface $connector
     * @param \Fusio\Impl\Service\Plan\Payer $payerService
     * @param \Fusio\Impl\Provider\ProviderFactory $providerFactory
     * @param \PSX\Framework\Config\Config $config
     * @param \Fusio\Impl\Table\Plan $planTable
     * @param \Fusio\Impl\Table\Transaction $transactionTable
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
     */
    public function __construct(ConnectorInterface $connector, Payer $payerService, ProviderFactory $providerFactory, Config $config, Table\Plan $planTable, Table\Transaction $transactionTable, EventDispatcherInterface $eventDispatcher)
    {
        $this->connector = $connector;
        $this->payerService = $payerService;
        $this->providerFactory = $providerFactory;
        $this->config = $config;
        $this->planTable = $planTable;
        $this->transactionTable = $transactionTable;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * @param string $name
     * @param integer $planId
     * @param string $returnUrl
     * @param \Fusio\Impl\Authorization\UserContext $context
     * @return string
     */
    public function prepare($name, $planId, $returnUrl, UserContext $context)
    {
        $provider = $this->providerFactory->factory($name);

        if (!$provider instanceof ProviderInterface) {
            throw new StatusCode\BadRequestException('Provider is not available');
        }

        $product    = $this->createProduct($planId);
        $connection = $this->connector->getConnection($name);

        // validate return url
        if (empty($returnUrl) || !filter_var($returnUrl, FILTER_VALIDATE_URL)) {
            throw new StatusCode\BadRequestException('Invalid return url');
        }

        // create transaction
        $transaction = new TransactionModel();
        $transaction->setPlanId($product->getId());
        $transaction->setUserId($context->getUserId());
        $transaction->setAppId($context->getAppId());
        $transaction->setProvider($name);
        $transaction->setTransactionId(Uuid::pseudoRandom());
        $transaction->setReturnUrl($returnUrl);

        $this->transactionTable->beginTransaction();

        try {
            // create transaction
            $this->transactionTable->create([
                'plan_id' => $transaction->getPlanId(),
                'user_id' => $transaction->getUserId(),
                'app_id' => $transaction->getAppId(),
                'status' => TransactionModel::STATUS_CREATED,
                'provider' => $transaction->getProvider(),
                'transaction_id' => $transaction->getTransactionId(),
                'amount' => $product->getPrice(),
                'return_url' => $transaction->getReturnUrl(),
                'insert_date' => new \DateTime(),
            ]);

            // set transaction id
            $transaction->setId($this->transactionTable->getLastInsertId());

            // prepare payment
            $approvalUrl = $provider->prepare(
                $connection,
                $product,
                $transaction,
                $this->buildRedirectUrls($transaction)
            );

            // update transaction
            $this->updateTransaction($product, $transaction);

            // trigger event
            $this->eventDispatcher->dispatch(TransactionEvents::PREPARE, new PreparedEvent($transaction));

            $this->transactionTable->commit();

            return $approvalUrl;
        } catch (\Throwable $e) {
            $this->transactionTable->rollBack();

            throw $e;
        }
    }

    /**
     * @param integer $transactionId
     * @param array $parameters
     * @return string
     */
    public function execute($transactionId, array $parameters)
    {
        $transaction = $this->createTransaction($transactionId);
        $provider    = $this->providerFactory->factory($transaction->getProvider());

        if (!$provider instanceof ProviderInterface) {
            throw new StatusCode\BadRequestException('Provider is not available');
        }

        $connection = $this->connector->getConnection($transaction->getProvider());

        $this->transactionTable->beginTransaction();

        try {
            // create product
            $product = $this->createProduct($transaction->getPlanId());

            // execute transaction
            $provider->execute($connection, $product, $transaction, $parameters);

            // update transaction
            $this->updateTransaction($product, $transaction);

            // trigger event
            $this->eventDispatcher->dispatch(TransactionEvents::EXECUTE, new ExecutedEvent($transaction));

            $this->transactionTable->commit();

            return $this->buildReturnUrl($transaction);
        } catch (\Throwable $e) {
            $this->transactionTable->rollBack();

            throw $e;
        }
    }

    /**
     * @param integer $planId
     * @return \Fusio\Engine\Model\ProductInterface
     */
    private function createProduct($planId)
    {
        $plan = $this->planTable->get($planId);

        if (empty($plan)) {
            throw new StatusCode\BadRequestException('Invalid plan id');
        }

        if ($plan['status'] != Table\Plan::STATUS_ACTIVE) {
            throw new StatusCode\BadRequestException('Invalid plan status');
        }

        $product = new Product();
        $product->setId($plan['id']);
        $product->setName($plan['name']);
        $product->setPrice($plan['price']);
        $product->setPoints($plan['points']);

        return $product;
    }

    /**
     * @param integer $transactionId
     * @return \Fusio\Engine\Model\Transaction
     */
    private function createTransaction($transactionId)
    {
        $condition = new Condition();
        $condition->equals('transaction_id', $transactionId);

        $result = $this->transactionTable->getOneBy($condition);

        if (empty($result)) {
            throw new StatusCode\BadRequestException('Invalid transaction id');
        }

        if ($result['status'] == TransactionInterface::STATUS_APPROVED) {
            throw new StatusCode\BadRequestException('Transaction is already approved');
        }

        $transaction = new TransactionModel();
        $transaction->setId($result['id']);
        $transaction->setPlanId($result['plan_id']);
        $transaction->setUserId($result['user_id']);
        $transaction->setAppId($result['app_id']);
        $transaction->setStatus($result['status']);
        $transaction->setProvider($result['provider']);
        $transaction->setTransactionId($result['transaction_id']);
        $transaction->setAmount($result['amount']);
        $transaction->setReturnUrl($result['return_url']);

        $updateDate = $result['update_date'];
        if (!empty($updateDate)) {
            $transaction->setUpdateDate($updateDate);
        }

        $transaction->setCreateDate($result['insert_date']);

        return $transaction;
    }

    /**
     * @param \Fusio\Engine\Model\ProductInterface $product
     * @param \Fusio\Engine\Model\TransactionInterface $transaction
     */
    private function updateTransaction(ProductInterface $product, TransactionInterface $transaction)
    {
        // update transaction
        $this->transactionTable->update([
            'id' => $transaction->getId(),
            'status' => $transaction->getStatus(),
            'remote_id' => $transaction->getRemoteId(),
            'update_date' => new \DateTime(),
        ]);

        // if approved add points to user
        if ($transaction->getStatus() == TransactionInterface::STATUS_APPROVED) {
            $this->payerService->credit(
                $product->getPoints(),
                UserContext::newContext($transaction->getUserId(), $transaction->getAppId())
            );
        }
    }

    /**
     * @param \Fusio\Engine\Model\TransactionInterface $transaction
     * @return \Fusio\Engine\Payment\RedirectUrls
     */
    private function buildRedirectUrls(TransactionInterface $transaction)
    {
        $baseUrl = $this->config->get('psx_url') . '/' . $this->config->get('psx_dispatch');

        $redirectUrls = new RedirectUrls();
        $redirectUrls->setReturnUrl($baseUrl . 'consumer/plan/payment/execute/' . $transaction->getTransactionId());
{'fraction_non_alphanumeric': 0.13209647495361782, 'fraction_numerical': 0.0010204081632653062, 'mean_word_length': 2.6982847341337908, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 244, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, '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': '5295896', 'n_tokens_mistral': 5728, 'n_tokens_neox': 5583, 'n_words': 1122}