. */ declare(strict_types=1); namespace FireflyIII\Factory; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Collection; /** * Class TransactionFactory */ class TransactionFactory { /** @var AccountRepositoryInterface */ private $accountRepository; /** @var BudgetRepositoryInterface */ private $budgetRepository; /** @var CategoryRepositoryInterface */ private $categoryRepository; /** @var CurrencyRepositoryInterface */ private $currencyRepository; /** @var JournalRepositoryInterface */ private $repository; /** @var User */ private $user; /** * TransactionFactory constructor. */ public function __construct() { $this->repository = app(JournalRepositoryInterface::class); $this->accountRepository = app(AccountRepositoryInterface::class); $this->budgetRepository = app(BudgetRepositoryInterface::class); $this->categoryRepository = app(CategoryRepositoryInterface::class); $this->currencyRepository = app(CurrencyRepositoryInterface::class); } /** * @param array $data * * @return Transaction */ public function create(array $data): Transaction { $foreignCurrencyId = is_null($data['foreign_currency']) ? null : $data['foreign_currency']->id; $values = [ 'reconciled' => $data['reconciled'], 'account_id' => $data['account']->id, 'transaction_journal_id' => $data['transaction_journal']->id, 'description' => $data['description'], 'transaction_currency_id' => $data['currency']->id, 'amount' => $data['amount'], 'foreign_amount' => $data['foreign_amount'], 'foreign_currency_id' => $foreignCurrencyId, 'identifier' => $data['identifier'], ]; $transaction = $this->repository->storeBasicTransaction($values); // todo: add budget, category, etc. // todo link budget, category return $transaction; } /** * Create a pair of transactions based on the data given in the array. * * @param TransactionJournal $journal * @param array $data * * @return Collection * @throws FireflyException */ public function createPair(TransactionJournal $journal, array $data): Collection { // all this data is the same for both transactions: $currency = $this->findCurrency($data['currency_id'], $data['currency_code']); $description = $journal->description === $data['description'] ? null : $data['description']; // type of source account depends on journal type: $sourceType = $this->accountType($journal, 'source'); $sourceAccount = $this->findAccount($sourceType, $data['source_id'], $data['source_name']); // same for destination account: $destinationType = $this->accountType($journal, 'destination'); $destinationAccount = $this->findAccount($destinationType, $data['destination_id'], $data['destination_name']); // first make a "negative" (source) transaction based on the data in the array. $sourceTransactionData = [ 'description' => $description, 'amount' => app('steam')->negative(strval($data['amount'])), 'foreign_amount' => null, 'currency' => $currency, 'foreign_currency' => null, 'budget' => null, 'category' => null, 'account' => $sourceAccount, 'transaction_journal' => $journal, 'reconciled' => $data['reconciled'], 'identifier' => $data['identifier'], ]; $source = $this->create($sourceTransactionData); // then make a "positive" transaction based on the data in the array. $destTransactionData = [ 'description' => $sourceTransactionData['description'], 'amount' => app('steam')->positive(strval($data['amount'])), 'foreign_amount' => null, 'currency' => $currency, 'foreign_currency' => null, 'budget' => null, 'category' => null, 'account' => $destinationAccount, 'transaction_journal' => $journal, 'reconciled' => $data['reconciled'], 'identifier' => $data['identifier'], ]; $dest = $this->create($destTransactionData); return new Collection([$source, $dest]); } /** * @param User $user */ public function setUser(User $user) { $this->user = $user; $this->repository->setUser($user); $this->accountRepository->setUser($user); $this->budgetRepository->setUser($user); $this->categoryRepository->setUser($user); $this->currencyRepository->setUser($user); } /** * @param TransactionJournal $journal * @param string $direction * * @return string * @throws FireflyException */ protected function accountType(TransactionJournal $journal, string $direction): string { $types = []; $type = $journal->transactionType->type; switch ($type) { default: throw new FireflyException(sprintf('Cannot handle type "%s" in accountType()', $type)); case TransactionType::WITHDRAWAL: $types['source'] = AccountType::ASSET; $types['destination'] = AccountType::EXPENSE; break; case TransactionType::DEPOSIT: $types['source'] = AccountType::REVENUE; $types['destination'] = AccountType::ASSET; break; case TransactionType::TRANSFER: $types['source'] = AccountType::ASSET; $types['destination'] = AccountType::ASSET; break; } if (!isset($types[$direction])) { throw new FireflyException(sprintf('No type set for direction "%s" and type "%s"', $type, $direction)); } return $types[$direction]; } /** * @param string $expectedType * @param int|null $accountId * @param string|null $accountName * * @return Account * @throws FireflyException */ protected function findAccount(string $expectedType, ?int $accountId, ?string $accountName): Account { $accountId = intval($accountId); $accountName = strval($accountName); switch ($expectedType) { case AccountType::ASSET: if ($accountId > 0) { // must be able to find it based on ID. Validator should catch invalid ID's. return $this->accountRepository->findNull($accountId); } // alternatively, return by name. Validator should catch invalid names. return $this->accountRepository->findByName($accountName, [AccountType::ASSET]); break; case AccountType::EXPENSE: if ($accountId > 0) { // must be able to find it based on ID. Validator should catch invalid ID's. return $this->accountRepository->findNull($accountId); } if (strlen($accountName) > 0) { // alternatively, return by name. Validator should catch invalid names. return $this->accountRepository->findByName($accountName, [AccountType::EXPENSE]); } // return cash account: return $this->accountRepository->getCashAccount(); break; case AccountType::REVENUE: if ($accountId > 0) { // must be able to find it based on ID. Validator should catch invalid ID's. return $this->accountRepository->findNull($accountId); } if (strlen($accountName) > 0) { // alternatively, return by name. Validator should catch invalid names. return $this->accountRepository->findByName($accountName, [AccountType::REVENUE]); } // return cash account: return $this->accountRepository->getCashAccount(); default: throw new FireflyException(sprintf('Cannot find account of type "%s".', $expectedType)); } } /** * @param int|null $budgetId * @param null|string $budgetName * * @return Budget|null */ protected function findBudget(?int $budgetId, ?string $budgetName): ?Budget { $budgetId = intval($budgetId); $budgetName = strval($budgetName); if (strlen($budgetName) === 0 && $budgetId === 0) { return null; } // first by ID: if ($budgetId > 0) { $budget = $this->budgetRepository->findNull($budgetId); if (!is_null($budget)) { return $budget; } } if (strlen($budgetName) > 0) { $budget = $this->budgetRepository->findByName($budgetName); if (!is_null($budget)) { return $budget; } } return null; } /** * @param int|null $categoryId * @param null|string $categoryName * * @return Category|null */ protected function findCategory(?int $categoryId, ?string $categoryName): ?Category { $categoryId = intval($categoryId); $categoryName = strval($categoryName); if (strlen($categoryName) === 0 && $categoryId === 0) { return null; } // first by ID: if ($categoryId > 0) { $category = $this->categoryRepository->findNull($categoryId); if (!is_null($category)) { return $category; } } if (strlen($categoryName) > 0) { $category = $this->categoryRepository->findByName($categoryName); if (!is_null($category)) { return $category; } // create it? die('create category'); } return null; } /** * @param int|null $currencyId * @param null|string $currencyCode * * @return TransactionCurrency|null */ protected function findCurrency(?int $currencyId, ?string $currencyCode): ?TransactionCurrency { $currencyCode = strval($currencyCode); $currencyId = intval($currencyId); if (strlen($currencyCode) === 0 && intval($currencyId) === 0) { return null; } // first by ID: if ($currencyId > 0) { $currency = $this->currencyRepository->findNull($currencyId); if (!is_null($currency)) { return $currency; } } // then by code: if (strlen($currencyCode) > 0) { $currency = $this->currencyRepository->findByCodeNull($currencyCode); if (!is_null($currency)) { return $currency; } } return null; } }