From 6823adc97667579bc93bb0207160b1f8a00fba2e Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 26 Oct 2019 07:49:12 +0200 Subject: [PATCH] Firefly III can automatically stop duplicate transactions from being created. --- .../V1/Controllers/TransactionController.php | 16 ++++++- .../V1/Requests/TransactionStoreRequest.php | 6 ++- .../DuplicateTransactionException.php | 31 +++++++++++++ app/Factory/TransactionGroupFactory.php | 4 ++ app/Factory/TransactionJournalFactory.php | 46 ++++++++++++++++++- .../TransactionGroupRepository.php | 2 + .../TransactionGroupRepositoryInterface.php | 2 + 7 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 app/Exceptions/DuplicateTransactionException.php diff --git a/app/Api/V1/Controllers/TransactionController.php b/app/Api/V1/Controllers/TransactionController.php index 545126cbdb..f948ad88bc 100644 --- a/app/Api/V1/Controllers/TransactionController.php +++ b/app/Api/V1/Controllers/TransactionController.php @@ -28,6 +28,7 @@ use FireflyIII\Api\V1\Requests\TransactionStoreRequest; use FireflyIII\Api\V1\Requests\TransactionUpdateRequest; use FireflyIII\Events\StoredTransactionGroup; use FireflyIII\Events\UpdatedTransactionGroup; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; @@ -279,7 +280,20 @@ class TransactionController extends Controller Log::channel('audit') ->info('Store new transaction over API.', $data); - $transactionGroup = $this->groupRepository->store($data); + try { + $transactionGroup = $this->groupRepository->store($data); + } catch (DuplicateTransactionException $e) { + // return bad validation message. + // TODO use Laravel's internal validation thing to do this. + $response = [ + 'message' => 'The given data was invalid.', + 'errors' => [ + 'transactions.0.description' => [$e->getMessage()], + ], + ]; + + return response()->json($response, 422); + } event(new StoredTransactionGroup($transactionGroup)); diff --git a/app/Api/V1/Requests/TransactionStoreRequest.php b/app/Api/V1/Requests/TransactionStoreRequest.php index 795eef6fca..07f5e77997 100644 --- a/app/Api/V1/Requests/TransactionStoreRequest.php +++ b/app/Api/V1/Requests/TransactionStoreRequest.php @@ -58,8 +58,9 @@ class TransactionStoreRequest extends Request public function getAll(): array { $data = [ - 'group_title' => $this->string('group_title'), - 'transactions' => $this->getTransactionData(), + 'group_title' => $this->string('group_title'), + 'error_if_duplicate_hash' => $this->boolean('error_if_duplicate_hash'), + 'transactions' => $this->getTransactionData(), ]; return $data; @@ -75,6 +76,7 @@ class TransactionStoreRequest extends Request $rules = [ // basic fields for group: 'group_title' => 'between:1,1000|nullable', + 'error_if_duplicate_hash' => [new IsBoolean], // transaction rules (in array for splits): 'transactions.*.type' => 'required|in:withdrawal,deposit,transfer,opening-balance,reconciliation', diff --git a/app/Exceptions/DuplicateTransactionException.php b/app/Exceptions/DuplicateTransactionException.php new file mode 100644 index 0000000000..8d2d62ddb1 --- /dev/null +++ b/app/Exceptions/DuplicateTransactionException.php @@ -0,0 +1,31 @@ +. + */ + +namespace FireflyIII\Exceptions; +use Exception; + +/** + * Class DuplicateTransactionException + */ +class DuplicateTransactionException extends Exception +{ + +} \ No newline at end of file diff --git a/app/Factory/TransactionGroupFactory.php b/app/Factory/TransactionGroupFactory.php index 91e7ee21fd..bb69039030 100644 --- a/app/Factory/TransactionGroupFactory.php +++ b/app/Factory/TransactionGroupFactory.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Factory; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Models\TransactionGroup; use FireflyIII\User; @@ -52,10 +53,13 @@ class TransactionGroupFactory * @param array $data * * @return TransactionGroup + * @throws DuplicateTransactionException */ public function create(array $data): TransactionGroup { $this->journalFactory->setUser($this->user); + $this->journalFactory->setErrorOnHash($data['error_if_duplicate_hash']); + $collection = $this->journalFactory->create($data); $title = $data['group_title'] ?? null; $title = '' === $title ? null : $title; diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index c7e79306d4..4bd58b8dcf 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -26,10 +26,12 @@ namespace FireflyIII\Factory; use Carbon\Carbon; use Exception; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Account; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionJournalMeta; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; @@ -72,6 +74,8 @@ class TransactionJournalFactory private $typeRepository; /** @var User The user */ private $user; + /** @var bool */ + private $errorOnHash; /** * Constructor. @@ -81,7 +85,8 @@ class TransactionJournalFactory */ public function __construct() { - $this->fields = [ + $this->errorOnHash = false; + $this->fields = [ // sepa 'sepa_cc', 'sepa_ct_op', 'sepa_ct_id', 'sepa_db', 'sepa_country', 'sepa_ep', @@ -119,6 +124,7 @@ class TransactionJournalFactory * @param array $data * * @return Collection + * @throws DuplicateTransactionException */ public function create(array $data): Collection { @@ -193,11 +199,15 @@ class TransactionJournalFactory * @param NullArrayObject $row * * @return TransactionJournal|null + * @throws Exception + * @throws DuplicateTransactionException */ private function createJournal(NullArrayObject $row): ?TransactionJournal { $row['import_hash_v2'] = $this->hashArray($row); + $this->errorIfDuplicate($row['import_hash_v2']); + /** Some basic fields */ $type = $this->typeRepository->findTransactionType(null, $row['type']); $carbon = $row['date'] ?? new Carbon; @@ -376,6 +386,30 @@ class TransactionJournalFactory return $journal; } + /** + * If this transaction already exists, throw an error. + * + * @param string $hash + * + * @throws DuplicateTransactionException + */ + private function errorIfDuplicate(string $hash): void + { + if (false === $this->errorOnHash) { + return; + } + $result = null; + if ($this->errorOnHash) { + /** @var TransactionJournalMeta $result */ + $result = TransactionJournalMeta::where('data', json_encode($hash, JSON_THROW_ON_ERROR)) + ->with(['transactionJournal', 'transactionJournal.transactionGroup']) + ->first(); + } + if (null !== $result) { + throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $result->transactionJournal->transaction_group_id)); + } + } + /** * @param TransactionCurrency|null $currency * @param Account $account @@ -485,4 +519,14 @@ class TransactionJournalFactory throw new FireflyException(sprintf('Destination: %s', $this->accountValidator->destError)); // @codeCoverageIgnore } } + + /** + * @param bool $errorOnHash + */ + public function setErrorOnHash(bool $errorOnHash): void + { + $this->errorOnHash = $errorOnHash; + } + + } diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php index d23853869c..5f7de565c7 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php @@ -27,6 +27,7 @@ namespace FireflyIII\Repositories\TransactionGroup; use Carbon\Carbon; use DB; use Exception; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Factory\TransactionGroupFactory; use FireflyIII\Models\AccountMeta; @@ -314,6 +315,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface * @param array $data * * @return TransactionGroup + * @throws DuplicateTransactionException */ public function store(array $data): TransactionGroup { diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php index d184aa8f27..58c0496c2a 100644 --- a/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php +++ b/app/Repositories/TransactionGroup/TransactionGroupRepositoryInterface.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\TransactionGroup; +use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Models\TransactionGroup; use FireflyIII\Support\NullArrayObject; use FireflyIII\User; @@ -125,6 +126,7 @@ interface TransactionGroupRepositoryInterface * @param array $data * * @return TransactionGroup + * @throws DuplicateTransactionException */ public function store(array $data): TransactionGroup;