From beece4dcbb7425562a01ea931d0f6b24125abbd3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 Mar 2021 16:08:49 +0100 Subject: [PATCH] Fix tests for transaction storage. --- .../Models/Recurrence/StoreRequest.php | 62 ++--- .../V1/Requests/Models/Rule/StoreRequest.php | 55 ++-- app/Factory/RecurrenceFactory.php | 61 ++++- app/Repositories/Rule/RuleRepository.php | 104 ++++++-- .../Rule/RuleRepositoryInterface.php | 13 + .../RuleGroup/RuleGroupRepository.php | 58 ++++- .../Support/RecurringTransactionTrait.php | 81 ++++-- app/Support/Request/GetRecurrenceData.php | 73 ++++-- app/Validation/RecurrenceValidation.php | 15 ++ config/logging.php | 2 +- .../Models/Recurrence/StoreControllerTest.php | 205 +++++++++++++++ tests/Api/Models/Rule/StoreControllerTest.php | 237 ++++++++++++++++++ .../Transaction/StoreControllerTest.php | 167 ++++++++++++ tests/Traits/TestHelpers.php | 101 ++++++-- 14 files changed, 1067 insertions(+), 167 deletions(-) create mode 100644 tests/Api/Models/Recurrence/StoreControllerTest.php create mode 100644 tests/Api/Models/Rule/StoreControllerTest.php create mode 100644 tests/Api/Models/Transaction/StoreControllerTest.php diff --git a/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php b/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php index 2fd320eaed..476b8ab4f4 100644 --- a/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Recurrence/StoreRequest.php @@ -43,7 +43,6 @@ class StoreRequest extends FormRequest use ConvertsDataTypes, RecurrenceValidation, TransactionValidation, CurrencyValidation, GetRecurrenceData, ChecksLogin; - /** * Get all data from the request. * @@ -51,26 +50,21 @@ class StoreRequest extends FormRequest */ public function getAll(): array { - $active = true; - $applyRules = true; - if (null !== $this->get('active')) { - $active = $this->boolean('active'); - } - if (null !== $this->get('apply_rules')) { - $applyRules = $this->boolean('apply_rules'); - } + $fields = [ + 'type' => ['type', 'string'], + 'title' => ['title', 'string'], + 'description' => ['description', 'string'], + 'first_date' => ['first_date', 'date'], + 'repeat_until' => ['repeat_until', 'date'], + 'nr_of_repetitions' => ['nr_of_repetitions', 'integer'], + 'apply_rules' => ['apply_rules', 'boolean'], + 'active' => ['active', 'boolean'], + 'notes' => ['notes', 'nlString'], + ]; + $recurrence = $this->getAllData($fields); return [ - 'recurrence' => [ - 'type' => $this->string('type'), - 'title' => $this->string('title'), - 'description' => $this->string('description'), - 'first_date' => $this->date('first_date'), - 'repeat_until' => $this->date('repeat_until'), - 'repetitions' => $this->integer('nr_of_repetitions'), - 'apply_rules' => $applyRules, - 'active' => $active, - ], + 'recurrence' => $recurrence, 'transactions' => $this->getTransactionData(), 'repetitions' => $this->getRepetitionData(), ]; @@ -93,7 +87,7 @@ class StoreRequest extends FormRequest } /** @var array $transaction */ foreach ($transactions as $transaction) { - $return[] = $this->getSingleRecurrenceData($transaction); + $return[] = $this->getSingleTransactionData($transaction); } return $return; @@ -115,12 +109,21 @@ class StoreRequest extends FormRequest } /** @var array $repetition */ foreach ($repetitions as $repetition) { - $return[] = [ - 'type' => $repetition['type'], - 'moment' => $repetition['moment'], - 'skip' => (int) $repetition['skip'], - 'weekend' => (int) $repetition['weekend'], - ]; + $current = []; + if (array_key_exists('type', $repetition)) { + $current['type'] = $repetition['type']; + } + if (array_key_exists('moment', $repetition)) { + $current['moment'] = $repetition['moment']; + } + if (array_key_exists('skip', $repetition)) { + $current['skip'] = (int)$repetition['skip']; + } + if (array_key_exists('weekend', $repetition)) { + $current['weekend'] = (int)$repetition['weekend']; + } + + $return[] = $current; } return $return; @@ -142,12 +145,12 @@ class StoreRequest extends FormRequest 'first_date' => 'required|date', 'apply_rules' => [new IsBoolean], 'active' => [new IsBoolean], - 'repeat_until' => sprintf('date|after:%s', $today->format('Y-m-d')), + 'repeat_until' => 'date', 'nr_of_repetitions' => 'numeric|between:1,31', 'repetitions.*.type' => 'required|in:daily,weekly,ndom,monthly,yearly', 'repetitions.*.moment' => 'between:0,10', - 'repetitions.*.skip' => 'required|numeric|between:0,31', - 'repetitions.*.weekend' => 'required|numeric|min:1|max:4', + 'repetitions.*.skip' => 'numeric|between:0,31', + 'repetitions.*.weekend' => 'numeric|min:1|max:4', 'transactions.*.description' => 'required|between:1,255', 'transactions.*.amount' => 'required|numeric|gt:0', 'transactions.*.foreign_amount' => 'numeric|gt:0', @@ -184,6 +187,7 @@ class StoreRequest extends FormRequest { $validator->after( function (Validator $validator) { + $this->validateRecurringConfig($validator); $this->validateOneRecurrenceTransaction($validator); $this->validateOneRepetition($validator); $this->validateRecurrenceRepetition($validator); diff --git a/app/Api/V1/Requests/Models/Rule/StoreRequest.php b/app/Api/V1/Requests/Models/Rule/StoreRequest.php index 3bbe2dac39..94446a49f2 100644 --- a/app/Api/V1/Requests/Models/Rule/StoreRequest.php +++ b/app/Api/V1/Requests/Models/Rule/StoreRequest.php @@ -40,7 +40,6 @@ class StoreRequest extends FormRequest use ConvertsDataTypes, GetRuleConfiguration, ChecksLogin; - /** * Get all data from the request. * @@ -48,31 +47,23 @@ class StoreRequest extends FormRequest */ public function getAll(): array { - $strict = true; - $active = true; - $stopProcessing = false; - if (null !== $this->get('active')) { - $active = $this->boolean('active'); - } - if (null !== $this->get('strict')) { - $strict = $this->boolean('strict'); - } - if (null !== $this->get('stop_processing')) { - $stopProcessing = $this->boolean('stop_processing'); - } - - return [ - 'title' => $this->string('title'), - 'description' => $this->string('description'), - 'rule_group_id' => $this->integer('rule_group_id'), - 'rule_group_title' => $this->string('rule_group_title'), - 'trigger' => $this->string('trigger'), - 'strict' => $strict, - 'stop_processing' => $stopProcessing, - 'active' => $active, - 'triggers' => $this->getRuleTriggers(), - 'actions' => $this->getRuleActions(), + $fields = [ + 'title' => ['title', 'string'], + 'description' => ['description', 'string'], + 'rule_group_id' => ['rule_group_id', 'integer'], + 'order' => ['order', 'integer'], + 'rule_group_title' => ['rule_group_title', 'string'], + 'trigger' => ['trigger', 'string'], + 'strict' => ['strict', 'boolean'], + 'stop_processing' => ['stop_processing', 'boolean'], + 'active' => ['active', 'boolean'], ]; + $data = $this->getAllData($fields); + + $data['triggers'] = $this->getRuleTriggers(); + $data['actions'] = $this->getRuleActions(); + + return $data; } /** @@ -87,8 +78,8 @@ class StoreRequest extends FormRequest $return[] = [ 'type' => $trigger['type'], 'value' => $trigger['value'], - 'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'false')), - 'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')), + 'active' => $this->convertBoolean((string)($trigger['active'] ?? 'false')), + 'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')), ]; } } @@ -108,8 +99,8 @@ class StoreRequest extends FormRequest $return[] = [ 'type' => $action['type'], 'value' => $action['value'], - 'active' => $this->convertBoolean((string) ($action['active'] ?? 'false')), - 'stop_processing' => $this->convertBoolean((string) ($action['stop_processing'] ?? 'false')), + 'active' => $this->convertBoolean((string)($action['active'] ?? 'false')), + 'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')), ]; } } @@ -134,7 +125,7 @@ class StoreRequest extends FormRequest return [ 'title' => 'required|between:1,100|uniqueObjectForUser:rules,title', 'description' => 'between:1,5000|nullable', - 'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title', + 'rule_group_id' => 'belongsToUser:rule_groups|required_without:rule_group_title', 'rule_group_title' => 'nullable|between:1,255|required_without:rule_group_id|belongsToUser:rule_groups,title', 'trigger' => 'required|in:store-journal,update-journal', 'triggers.*.type' => 'required|in:' . implode(',', $validTriggers), @@ -179,7 +170,7 @@ class StoreRequest extends FormRequest $triggers = $data['triggers'] ?? []; // need at least one trigger if (!is_countable($triggers) || 0 === count($triggers)) { - $validator->errors()->add('title', (string) trans('validation.at_least_one_trigger')); + $validator->errors()->add('title', (string)trans('validation.at_least_one_trigger')); } } @@ -194,7 +185,7 @@ class StoreRequest extends FormRequest $actions = $data['actions'] ?? []; // need at least one trigger if (!is_countable($actions) || 0 === count($actions)) { - $validator->errors()->add('title', (string) trans('validation.at_least_one_action')); + $validator->errors()->add('title', (string)trans('validation.at_least_one_action')); } } } diff --git a/app/Factory/RecurrenceFactory.php b/app/Factory/RecurrenceFactory.php index 6549a23d6a..4ce667d307 100644 --- a/app/Factory/RecurrenceFactory.php +++ b/app/Factory/RecurrenceFactory.php @@ -41,8 +41,9 @@ class RecurrenceFactory { use TransactionTypeTrait, RecurringTransactionTrait; + private MessageBag $errors; - private User $user; + private User $user; /** @@ -58,8 +59,8 @@ class RecurrenceFactory /** * @param array $data * - * @throws FireflyException * @return Recurrence + * @throws FireflyException */ public function create(array $data): Recurrence { @@ -72,26 +73,60 @@ class RecurrenceFactory throw new FireflyException($message); } - /** @var Carbon $firstDate */ - $firstDate = $data['recurrence']['first_date']; + $firstDate = null; + $repeatUntil = null; + $repetitions = 0; + $title = null; + $description = ''; + $applyRules = true; + $active = true; + if (array_key_exists('first_date', $data['recurrence'])) { + /** @var Carbon $firstDate */ + $firstDate = $data['recurrence']['first_date']; + } + if (array_key_exists('nr_of_repetitions', $data['recurrence'])) { + $repetitions = (int)$data['recurrence']['nr_of_repetitions']; + } + if (array_key_exists('repeat_until', $data['recurrence'])) { + $repeatUntil = $data['recurrence']['repeat_until']; + } + if (array_key_exists('title', $data['recurrence'])) { + $title = $data['recurrence']['title']; + } + if (array_key_exists('description', $data['recurrence'])) { + $description = $data['recurrence']['description']; + } + if (array_key_exists('apply_rules', $data['recurrence'])) { + $applyRules = $data['recurrence']['apply_rules']; + } + if (array_key_exists('active', $data['recurrence'])) { + $active = $data['recurrence']['active']; + } + if ($repetitions > 0 && null === $repeatUntil) { + $repeatUntil = Carbon::create()->addyear(); + } - $repetitions = (int) $data['recurrence']['repetitions']; - $recurrence = new Recurrence( + $recurrence = new Recurrence( [ 'user_id' => $this->user->id, 'transaction_type_id' => $type->id, - 'title' => $data['recurrence']['title'], - 'description' => $data['recurrence']['description'], - 'first_date' => $firstDate->format('Y-m-d'), - 'repeat_until' => $repetitions > 0 ? null : $data['recurrence']['repeat_until'], + 'title' => $title, + 'description' => $description, + 'first_date' => $firstDate ? $firstDate->format('Y-m-d') : null, + 'repeat_until' => $repetitions > 0 ? null : $repeatUntil->format('Y-m-d'), 'latest_date' => null, - 'repetitions' => $data['recurrence']['repetitions'], - 'apply_rules' => $data['recurrence']['apply_rules'], - 'active' => $data['recurrence']['active'], + 'repetitions' => $repetitions, + 'apply_rules' => $applyRules, + 'active' => $active, ] ); $recurrence->save(); + if (array_key_exists('notes', $data['recurrence'])) { + $this->updateNote($recurrence, (string)$data['recurrence']['notes']); + + } + $this->createRepetitions($recurrence, $data['repetitions'] ?? []); try { $this->createTransactions($recurrence, $data['transactions'] ?? []); diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index cf0b6409bb..8c83111d22 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -28,9 +28,11 @@ use FireflyIII\Models\Rule; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleTrigger; +use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use FireflyIII\Support\Search\OperatorQuerySearch; use FireflyIII\User; use Illuminate\Support\Collection; +use Log; /** * Class RuleRepository. @@ -367,19 +369,9 @@ class RuleRepository implements RuleRepositoryInterface */ public function resetRuleOrder(RuleGroup $ruleGroup): bool { - $ruleGroup->rules()->withTrashed()->whereNotNull('deleted_at')->update(['order' => 0]); - - $set = $ruleGroup->rules() - ->orderBy('order', 'ASC') - ->orderBy('updated_at', 'DESC') - ->get(); - $count = 1; - /** @var Rule $entry */ - foreach ($set as $entry) { - $entry->order = $count; - $entry->save(); - ++$count; - } + $groupRepository = app(RuleGroupRepositoryInterface::class); + $groupRepository->setUser($ruleGroup->user); + $groupRepository->resetRuleOrder($ruleGroup); return true; } @@ -407,6 +399,43 @@ class RuleRepository implements RuleRepositoryInterface $this->user = $user; } + + /** + * @inheritDoc + */ + public function setOrder(Rule $rule, int $newOrder): void + { + $oldOrder = (int)$rule->order; + $groupId = (int)$rule->rule_group_id; + $maxOrder = $this->maxOrder($rule->ruleGroup); + $newOrder = $newOrder > $maxOrder ? $maxOrder + 1 : $newOrder; + Log::debug(sprintf('New order will be %d', $newOrder)); + + if ($newOrder > $oldOrder) { + $this->user->rules() + ->where('rules.rule_group_id', $groupId) + ->where('rules.order', '<=', $newOrder) + ->where('rules.order', '>', $oldOrder) + ->where('rules.id', '!=', $rule->id) + ->decrement('rules.order', 1); + $rule->order = $newOrder; + Log::debug(sprintf('Order of rule #%d ("%s") is now %d', $rule->id, $rule->title, $newOrder)); + $rule->save(); + + return; + } + + $this->user->rules() + ->where('rules.rule_group_id', $groupId) + ->where('rules.order', '>=', $newOrder) + ->where('rules.order', '<', $oldOrder) + ->where('rules.id', '!=', $rule->id) + ->increment('rules.order', 1); + $rule->order = $newOrder; + Log::debug(sprintf('Order of rule #%d ("%s") is now %d', $rule->id, $rule->title, $newOrder)); + $rule->save(); + } + /** * @param array $data * @@ -414,31 +443,48 @@ class RuleRepository implements RuleRepositoryInterface */ public function store(array $data): Rule { - /** @var RuleGroup $ruleGroup */ - $ruleGroup = $this->user->ruleGroups()->find($data['rule_group_id']); - - // get max order: - $order = $this->getHighestOrderInRuleGroup($ruleGroup); + $ruleGroup = null; + if (array_key_exists('rule_group_id', $data)) { + $ruleGroup = $this->user->ruleGroups()->find($data['rule_group_id']); + } + if (array_key_exists('rule_group_title', $data)) { + $ruleGroup = $this->user->ruleGroups()->where('title', $data['rule_group_title'])->first(); + } + if (null === $ruleGroup) { + throw new FireflyException('No such rule group.'); + } // start by creating a new rule: $rule = new Rule; $rule->user()->associate($this->user->id); - $rule->rule_group_id = $data['rule_group_id']; - $rule->order = ($order + 1); - $rule->active = $data['active']; - $rule->strict = $data['strict']; - $rule->stop_processing = $data['stop_processing']; + $rule->rule_group_id = $ruleGroup->id; + $rule->order = 31337; + $rule->active = array_key_exists('active', $data) ? $data['active'] : true; + $rule->strict = array_key_exists('strict', $data) ? $data['strict'] : false; + $rule->stop_processing = array_key_exists('stop_processing', $data) ? $data['stop_processing'] : false; $rule->title = $data['title']; - $rule->description = strlen($data['description']) > 0 ? $data['description'] : null; - + $rule->description = array_key_exists('stop_processing', $data) ? $data['stop_processing'] : null; $rule->save(); + $rule->refresh(); + + // save update trigger: + $this->setRuleTrigger($data['trigger'] ?? 'store-journal', $rule); + + // reset order: + $this->resetRuleOrder($ruleGroup); + Log::debug('Done with resetting.'); + if (array_key_exists('order', $data)) { + Log::debug(sprintf('User has submitted order %d', $data['order'])); + $this->setOrder($rule, $data['order']); + } // start storing triggers: $this->storeTriggers($rule, $data); // same for actions. $this->storeActions($rule, $data); + $rule->refresh(); return $rule; } @@ -614,4 +660,12 @@ class RuleRepository implements RuleRepositoryInterface $trigger->stop_processing = false; $trigger->save(); } + + /** + * @inheritDoc + */ + public function maxOrder(RuleGroup $ruleGroup): int + { + return (int)$ruleGroup->rules()->max('order'); + } } diff --git a/app/Repositories/Rule/RuleRepositoryInterface.php b/app/Repositories/Rule/RuleRepositoryInterface.php index f9e7650108..1e25e11541 100644 --- a/app/Repositories/Rule/RuleRepositoryInterface.php +++ b/app/Repositories/Rule/RuleRepositoryInterface.php @@ -189,6 +189,19 @@ interface RuleRepositoryInterface */ public function store(array $data): Rule; + /** + * @param Rule $rule + * @param int $newOrder + */ + public function setOrder(Rule $rule, int $newOrder): void; + + /** + * @param RuleGroup $ruleGroup + * + * @return int + */ + public function maxOrder(RuleGroup $ruleGroup): int; + /** * @param Rule $rule * @param array $values diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index e8031a3923..6981d3cb7d 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -24,7 +24,9 @@ namespace FireflyIII\Repositories\RuleGroup; use Exception; use FireflyIII\Models\Rule; +use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleGroup; +use FireflyIII\Models\RuleTrigger; use FireflyIII\User; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; @@ -365,10 +367,13 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface $count = 1; /** @var Rule $entry */ foreach ($set as $entry) { - if ($entry->order !== $count) { + if ((int)$entry->order !== $count) { + Log::debug(sprintf('Rule #%d was on spot %d but must be on spot %d', $entry->id, $entry->order, $count)); $entry->order = $count; $entry->save(); } + $this->resetRuleActionOrder($entry); + $this->resetRuleTriggerOrder($entry); ++$count; } @@ -376,6 +381,51 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface return true; } + /** + * @param Rule $rule + */ + private function resetRuleActionOrder(Rule $rule): void + { + $actions = $rule->ruleActions() + ->orderBy('order', 'ASC') + ->orderBy('active', 'DESC') + ->orderBy('action_type', 'ASC') + ->get(); + $index = 1; + /** @var RuleAction $action */ + foreach ($actions as $action) { + if ((int)$action->order !== $index) { + $action->order = $index; + $action->save(); + Log::debug(sprintf('Rule action #%d was on spot %d but must be on spot %d', $action->id, $action->order, $index)); + } + $index++; + } + } + + /** + * @param Rule $rule + */ + private function resetRuleTriggerOrder(Rule $rule): void + { + $triggers = $rule->ruleTriggers() + ->orderBy('order', 'ASC') + ->orderBy('active', 'DESC') + ->orderBy('trigger_type', 'ASC') + ->get(); + $index = 1; + /** @var RuleTrigger $trigger */ + foreach ($triggers as $trigger) { + $order = (int) $trigger->order; + if ($order !== $index) { + $trigger->order = $index; + $trigger->save(); + Log::debug(sprintf('Rule trigger #%d was on spot %d but must be on spot %d', $trigger->id, $order, $index)); + } + $index++; + } + } + /** * @inheritDoc */ @@ -412,12 +462,14 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface 'title' => $data['title'], 'description' => $data['description'], 'order' => 31337, - 'active' => $data['active'], + 'active' => array_key_exists('active', $data) ? $data['active'] : true, ] ); $newRuleGroup->save(); $this->resetOrder(); - $this->setOrder($newRuleGroup, $data['order']); + if (array_key_exists('order', $data)) { + $this->setOrder($newRuleGroup, $data['order']); + } return $newRuleGroup; } diff --git a/app/Services/Internal/Support/RecurringTransactionTrait.php b/app/Services/Internal/Support/RecurringTransactionTrait.php index 4c2c727f9e..e58312b75b 100644 --- a/app/Services/Internal/Support/RecurringTransactionTrait.php +++ b/app/Services/Internal/Support/RecurringTransactionTrait.php @@ -32,6 +32,7 @@ use FireflyIII\Factory\PiggyBankFactory; use FireflyIII\Factory\TransactionCurrencyFactory; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\Note; use FireflyIII\Models\Recurrence; use FireflyIII\Models\RecurrenceMeta; use FireflyIII\Models\RecurrenceRepetition; @@ -61,7 +62,7 @@ trait RecurringTransactionTrait 'recurrence_id' => $recurrence->id, 'repetition_type' => $array['type'], 'repetition_moment' => $array['moment'] ?? '', - 'repetition_skip' => $array['skip'], + 'repetition_skip' => $array['skip'] ?? 0, 'weekend' => $array['weekend'] ?? 1, ] ); @@ -69,6 +70,38 @@ trait RecurringTransactionTrait } } + + /** + * @param Recurrence $recurrence + * @param string $note + * + * @return bool + */ + public function updateNote(Recurrence $recurrence, string $note): bool + { + if ('' === $note) { + $dbNote = $recurrence->notes()->first(); + if (null !== $dbNote) { + try { + $dbNote->delete(); + } catch (Exception $e) { + Log::debug(sprintf('Error deleting note: %s', $e->getMessage())); + } + } + + return true; + } + $dbNote = $recurrence->notes()->first(); + if (null === $dbNote) { + $dbNote = new Note(); + $dbNote->noteable()->associate($recurrence); + } + $dbNote->text = trim($note); + $dbNote->save(); + + return true; + } + /** * Store transactions of a recurring transactions. It's complex but readable. * @@ -82,8 +115,8 @@ trait RecurringTransactionTrait foreach ($transactions as $array) { $sourceTypes = config(sprintf('firefly.expected_source_types.source.%s', $recurrence->transactionType->type)); $destTypes = config(sprintf('firefly.expected_source_types.destination.%s', $recurrence->transactionType->type)); - $source = $this->findAccount($sourceTypes, $array['source_id'], $array['source_name']); - $destination = $this->findAccount($destTypes, $array['destination_id'], $array['destination_name']); + $source = $this->findAccount($sourceTypes, $array['source_id'], null); + $destination = $this->findAccount($destTypes, $array['destination_id'], null); /** @var TransactionCurrencyFactory $factory */ $factory = app(TransactionCurrencyFactory::class); @@ -107,7 +140,6 @@ trait RecurringTransactionTrait } // TODO typeOverrule: the account validator may have another opinion on the transaction type. - $transaction = new RecurrenceTransaction( [ 'recurrence_id' => $recurrence->id, @@ -116,30 +148,36 @@ trait RecurringTransactionTrait 'source_id' => $source->id, 'destination_id' => $destination->id, 'amount' => $array['amount'], - 'foreign_amount' => '' === (string)$array['foreign_amount'] ? null : (string)$array['foreign_amount'], + 'foreign_amount' => array_key_exists('foreign_amount', $array) ? (string)$array['foreign_amount'] : null, 'description' => $array['description'], ] ); $transaction->save(); - /** @var BudgetFactory $budgetFactory */ - $budgetFactory = app(BudgetFactory::class); - $budgetFactory->setUser($recurrence->user); - $budget = $budgetFactory->find($array['budget_id'], $array['budget_name']); + $budget = null; + if (array_key_exists('budget_id', $array)) { + /** @var BudgetFactory $budgetFactory */ + $budgetFactory = app(BudgetFactory::class); + $budgetFactory->setUser($recurrence->user); + $budget = $budgetFactory->find($array['budget_id'], null); + } - /** @var CategoryFactory $categoryFactory */ - $categoryFactory = app(CategoryFactory::class); - $categoryFactory->setUser($recurrence->user); - $category = $categoryFactory->findOrCreate($array['category_id'], $array['category_name']); + $category = null; + if (array_key_exists('category_id', $array)) { + /** @var CategoryFactory $categoryFactory */ + $categoryFactory = app(CategoryFactory::class); + $categoryFactory->setUser($recurrence->user); + $category = $categoryFactory->findOrCreate($array['category_id'], null); + } // same for piggy bank - $piggyId = (int)($array['piggy_bank_id'] ?? 0.0); - $piggyName = $array['piggy_bank_name'] ?? ''; - $this->updatePiggyBank($transaction, $piggyId, $piggyName); + if (array_key_exists('piggy_bank_id', $array)) { + $this->updatePiggyBank($transaction, (int)$array['piggy_bank_id']); + } - // same for tags - $tags = $array['tags'] ?? []; - $this->updateTags($transaction, $tags); + if(array_key_exists('tags', $array)) { + $this->updateTags($transaction, $array['tags']); + } // create recurrence transaction meta: if (null !== $budget) { @@ -246,14 +284,13 @@ trait RecurringTransactionTrait /** * @param RecurrenceTransaction $transaction * @param int $piggyId - * @param string $piggyName */ - protected function updatePiggyBank(RecurrenceTransaction $transaction, int $piggyId, string $piggyName): void + protected function updatePiggyBank(RecurrenceTransaction $transaction, int $piggyId): void { /** @var PiggyBankFactory $factory */ $factory = app(PiggyBankFactory::class); $factory->setUser($transaction->recurrence->user); - $piggyBank = $factory->find($piggyId, $piggyName); + $piggyBank = $factory->find($piggyId, null); if (null !== $piggyBank) { /** @var RecurrenceMeta $entry */ $entry = $transaction->recurrenceTransactionMeta()->where('name', 'piggy_bank_id')->first(); diff --git a/app/Support/Request/GetRecurrenceData.php b/app/Support/Request/GetRecurrenceData.php index a58f223391..9b8ffe0dc5 100644 --- a/app/Support/Request/GetRecurrenceData.php +++ b/app/Support/Request/GetRecurrenceData.php @@ -33,31 +33,58 @@ trait GetRecurrenceData * * @return array */ - protected function getSingleRecurrenceData(array $transaction): array + protected function getSingleTransactionData(array $transaction): array { - return [ - 'amount' => $transaction['amount'], - 'currency_id' => isset($transaction['currency_id']) ? (int) $transaction['currency_id'] : null, - 'currency_code' => $transaction['currency_code'] ?? null, - 'foreign_amount' => $transaction['foreign_amount'] ?? null, - 'foreign_currency_id' => isset($transaction['foreign_currency_id']) ? (int) $transaction['foreign_currency_id'] : null, - 'foreign_currency_code' => $transaction['foreign_currency_code'] ?? null, - 'source_id' => isset($transaction['source_id']) ? (int) $transaction['source_id'] : null, - 'source_name' => isset($transaction['source_name']) ? (string) $transaction['source_name'] : null, - 'destination_id' => isset($transaction['destination_id']) ? (int) $transaction['destination_id'] : null, - 'destination_name' => isset($transaction['destination_name']) ? (string) $transaction['destination_name'] : null, - 'description' => $transaction['description'], - 'type' => $this->string('type'), + $return = []; - // new and updated fields: - 'piggy_bank_id' => isset($transaction['piggy_bank_id']) ? (int) $transaction['piggy_bank_id'] : null, - 'piggy_bank_name' => $transaction['piggy_bank_name'] ?? null, - 'tags' => $transaction['tags'] ?? [], - 'budget_id' => isset($transaction['budget_id']) ? (int) $transaction['budget_id'] : null, - 'budget_name' => $transaction['budget_name'] ?? null, - 'category_id' => isset($transaction['category_id']) ? (int) $transaction['category_id'] : null, - 'category_name' => $transaction['category_name'] ?? null, - ]; + // amount + currency + if (array_key_exists('amount', $transaction)) { + $return['amount'] = $transaction['amount']; + } + if (array_key_exists('currency_id', $transaction)) { + $return['currency_id'] = (int)$transaction['currency_id']; + } + if (array_key_exists('currency_code', $transaction)) { + $return['currency_code'] = $transaction['currency_code']; + } + + // foreign amount + currency + if (array_key_exists('foreign_amount', $transaction)) { + $return['foreign_amount'] = $transaction['foreign_amount']; + } + if (array_key_exists('foreign_currency_id', $transaction)) { + $return['foreign_currency_id'] = (int)$transaction['foreign_currency_id']; + } + if (array_key_exists('foreign_currency_code', $transaction)) { + $return['foreign_currency_code'] = $transaction['foreign_currency_code']; + } + // source + dest + if (array_key_exists('source_id', $transaction)) { + $return['source_id'] = (int)$transaction['source_id']; + } + if (array_key_exists('destination_id', $transaction)) { + $return['destination_id'] = (int)$transaction['destination_id']; + } + // description + if (array_key_exists('description', $transaction)) { + $return['description'] = $transaction['description']; + } + + if (array_key_exists('piggy_bank_id', $transaction)) { + $return['piggy_bank_id'] = (int)$transaction['piggy_bank_id']; + } + + if (array_key_exists('tags', $transaction)) { + $return['tags'] = $transaction['tags']; + } + if (array_key_exists('budget_id', $transaction)) { + $return['budget_id'] = (int)$transaction['budget_id']; + } + if (array_key_exists('category_id', $transaction)) { + $return['category_id'] = (int)$transaction['category_id']; + } + + return $return; } } diff --git a/app/Validation/RecurrenceValidation.php b/app/Validation/RecurrenceValidation.php index 9c37f84367..a58d4d2033 100644 --- a/app/Validation/RecurrenceValidation.php +++ b/app/Validation/RecurrenceValidation.php @@ -36,7 +36,22 @@ use Log; */ trait RecurrenceValidation { + public function validateRecurringConfig(Validator $validator) { + $data = $validator->getData(); + $reps = array_key_exists('nr_of_repetitions', $data) ? (int)$data['nr_of_repetitions'] : null; + $repeatUntil = array_key_exists('repeat_until', $data) ? new Carbon($data['repeat_until']) : null; + if(null === $reps && null === $repeatUntil) { + $validator->errors()->add('nr_of_repetitions', trans('validation.require_repeat_until')); + $validator->errors()->add('repeat_until', trans('validation.require_repeat_until')); + return; + } + if($reps > 0 && null !== $repeatUntil) { + $validator->errors()->add('nr_of_repetitions', trans('validation.require_repeat_until')); + $validator->errors()->add('repeat_until', trans('validation.require_repeat_until')); + return; + } + } /** * Validate account information input for recurrences which are being updated. diff --git a/config/logging.php b/config/logging.php index 472c02caad..1acddbcdc4 100644 --- a/config/logging.php +++ b/config/logging.php @@ -92,7 +92,7 @@ return [ 'driver' => 'single', 'path' => 'php://stdout', 'tap' => [AuditLogger::class], - 'level' => envNonEmpty('APP_LOG_LEVEL', 'info'), + 'level' => envNonEmpty('AUDIT_LOG_LEVEL', 'info'), ], 'dailytest' => [ 'driver' => 'daily', diff --git a/tests/Api/Models/Recurrence/StoreControllerTest.php b/tests/Api/Models/Recurrence/StoreControllerTest.php new file mode 100644 index 0000000000..91a4163b3a --- /dev/null +++ b/tests/Api/Models/Recurrence/StoreControllerTest.php @@ -0,0 +1,205 @@ +. + */ + +namespace Tests\Api\Models\Recurrence; + + +use Faker\Factory; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; +use Tests\Traits\CollectsValues; +use Tests\Traits\RandomValues; +use Tests\Traits\TestHelpers; + +/** + * Class StoreControllerTest + */ +class StoreControllerTest extends TestCase +{ + use RandomValues, TestHelpers, CollectsValues; + + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + + /** + * @param array $submission + * + * emptyDataProvider / storeDataProvider + * + * @dataProvider storeDataProvider + */ + public function testStore(array $submission): void + { + if ([] === $submission) { + $this->markTestSkipped('Empty data provider'); + } + $route = 'api.v1.recurrences.store'; + $this->storeAndCompare($route, $submission); + } + + /** + * @return array + */ + public function emptyDataProvider(): array + { + return [[[]]]; + + } + + /** + * @return array + */ + public function storeDataProvider(): array + { + $minimalSets = $this->minimalSets(); + $optionalSets = $this->optionalSets(); + $regenConfig = [ + 'title' => function () { + $faker = Factory::create(); + + return $faker->uuid; + }, + ]; + + return $this->genericDataProvider($minimalSets, $optionalSets, $regenConfig); + } + + /** + * @return array + */ + private function minimalSets(): array + { + $faker = Factory::create(); + // three sets: + $combis = [ + ['withdrawal', 1, 8], + ['deposit', 9, 1], + ['transfer', 1, 2], + ]; + + $types = [ + ['daily', ''], + ['weekly', (string)$faker->numberBetween(1, 7)], + ['ndom', (string)$faker->numberBetween(1, 4) . ',' . $faker->numberBetween(1, 7)], + ['monthly', (string)$faker->numberBetween(1, 31)], + ['yearly', $faker->date()], + ]; + $set = []; + + foreach ($combis as $combi) { + foreach ($types as $type) { + $set[] = [ + 'parameters' => [], + 'fields' => [ + 'type' => $combi[0], + 'title' => $faker->uuid, + 'first_date' => $faker->date(), + 'repeat_until' => $faker->date(), + 'repetitions' => [ + [ + 'type' => $type[0], + 'moment' => $type[1], + ], + ], + 'transactions' => [ + [ + 'description' => $faker->uuid, + 'amount' => number_format($faker->randomFloat(2, 10, 100), 2), + 'source_id' => $combi[1], + 'destination_id' => $combi[2], + ], + ], + ], + ]; + } + } + + return $set; + } + + + /** + * @return \array[][] + */ + private function optionalSets(): array + { + $faker = Factory::create(); + + return [ + 'description' => [ + 'fields' => [ + 'description' => $faker->uuid, + ], + ], + 'nr_of_repetitions' => [ + 'fields' => [ + 'nr_of_repetitions' => $faker->numberBetween(1, 2), + ], + 'remove_fields' => ['repeat_until'], + ], + 'apply_rules' => [ + 'fields' => [ + 'apply_rules' => $faker->boolean, + ], + ], + 'active' => [ + 'fields' => [ + 'active' => $faker->boolean, + ], + ], + 'notes' => [ + 'fields' => [ + 'notes' => $faker->uuid, + ], + ], + 'repetitions_skip' => [ + 'fields' => [ + 'repetitions' => [ + // first entry, set field: + [ + 'skip' => $faker->numberBetween(1,3), + ], + ], + ], + ], + 'repetitions_weekend' => [ + 'fields' => [ + 'repetitions' => [ + // first entry, set field: + [ + 'weekend' => $faker->numberBetween(1,4), + ], + ], + ], + ] + ]; + } + +} \ No newline at end of file diff --git a/tests/Api/Models/Rule/StoreControllerTest.php b/tests/Api/Models/Rule/StoreControllerTest.php new file mode 100644 index 0000000000..ff579d713a --- /dev/null +++ b/tests/Api/Models/Rule/StoreControllerTest.php @@ -0,0 +1,237 @@ +. + */ + +namespace Tests\Api\Models\Rule; + + +use Faker\Factory; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; +use Tests\Traits\CollectsValues; +use Tests\Traits\RandomValues; +use Tests\Traits\TestHelpers; + +/** + * Class StoreControllerTest + */ +class StoreControllerTest extends TestCase +{ + use RandomValues, TestHelpers, CollectsValues; + + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + + /** + * @param array $submission + * + * emptyDataProvider / storeDataProvider + * + * @dataProvider storeDataProvider + */ + public function testStore(array $submission): void + { + if ([] === $submission) { + $this->markTestSkipped('Empty data provider'); + } + $route = 'api.v1.rules.store'; + $this->storeAndCompare($route, $submission); + } + + /** + * @return array + */ + public function emptyDataProvider(): array + { + return [[[]]]; + + } + + /** + * @return array + */ + public function storeDataProvider(): array + { + $minimalSets = $this->minimalSets(); + $optionalSets = $this->optionalSets(); + $regenConfig = [ + 'title' => function () { + $faker = Factory::create(); + + return $faker->uuid; + }, + ]; + + return $this->genericDataProvider($minimalSets, $optionalSets, $regenConfig); + } + + /** + * @return array + */ + private function minimalSets(): array + { + $faker = Factory::create(); + // - title + // - rule_group_id + // - trigger + // - triggers + // - actions + $set = [ + 'default_by_id' => [ + 'parameters' => [], + 'fields' => [ + 'title' => $faker->uuid, + 'rule_group_id' => (string)$faker->randomElement([1, 2]), + 'trigger' => $faker->randomElement(['store-journal', 'update-journal']), + 'triggers' => [ + [ + 'type' => $faker->randomElement(['from_account_starts', 'from_account_is', 'description_ends', 'description_is']), + 'value' => $faker->uuid, + ], + ], + 'actions' => [ + [ + 'type' => $faker->randomElement(['set_category', 'add_tag', 'set_description']), + 'value' => $faker->uuid, + ], + ], + ], + ], + 'default_by_title' => [ + 'parameters' => [], + 'fields' => [ + 'title' => $faker->uuid, + 'rule_group_title' => sprintf('Rule group %d', $faker->randomElement([1, 2])), + 'trigger' => $faker->randomElement(['store-journal', 'update-journal']), + 'triggers' => [ + [ + 'type' => $faker->randomElement(['from_account_starts', 'from_account_is', 'description_ends', 'description_is']), + 'value' => $faker->uuid, + ], + ], + 'actions' => [ + [ + 'type' => $faker->randomElement(['set_category', 'add_tag', 'set_description']), + 'value' => $faker->uuid, + ], + ], + ], + ], + ]; + + // leave it like this for now. + + return $set; + + + } + + + /** + * @return \array[][] + */ + private function optionalSets(): array + { + $faker = Factory::create(); + + return [ + 'order' => [ + 'fields' => [ + 'order' => $faker->numberBetween(1, 2), + ], + ], + 'active' => [ + 'fields' => [ + 'active' => $faker->boolean, + ], + ], + 'strict' => [ + 'fields' => [ + 'strict' => $faker->boolean, + ], + ], + 'stop_processing' => [ + 'fields' => [ + 'stop_processing' => $faker->boolean, + ], + ], + 'triggers_order' => [ + 'fields' => [ + 'triggers' => [ + // first entry, set field: + [ + 'order' => 1, + ], + ], + ], + ], + 'triggers_active' => [ + 'fields' => [ + 'triggers' => [ + // first entry, set field: + [ + 'active' => false, + ], + ], + ], + ], + 'triggers_not_active' => [ + 'fields' => [ + 'triggers' => [ + // first entry, set field: + [ + 'active' => true, + ], + ], + ], + ], + 'triggers_processing' => [ + 'fields' => [ + 'triggers' => [ + // first entry, set field: + [ + 'stop_processing' => true, + ], + ], + ], + ], + 'triggers_not_processing' => [ + 'fields' => [ + 'triggers' => [ + // first entry, set field: + [ + 'stop_processing' => false, + ], + ], + ], + ], + ]; + } + +} \ No newline at end of file diff --git a/tests/Api/Models/Transaction/StoreControllerTest.php b/tests/Api/Models/Transaction/StoreControllerTest.php new file mode 100644 index 0000000000..3a89805f4b --- /dev/null +++ b/tests/Api/Models/Transaction/StoreControllerTest.php @@ -0,0 +1,167 @@ +. + */ + +namespace Tests\Api\Models\Transaction; + + +use Faker\Factory; +use Laravel\Passport\Passport; +use Log; +use Tests\TestCase; +use Tests\Traits\CollectsValues; +use Tests\Traits\RandomValues; +use Tests\Traits\TestHelpers; + +/** + * Class StoreControllerTest + */ +class StoreControllerTest extends TestCase +{ + use RandomValues, TestHelpers, CollectsValues; + + /** + * + */ + public function setUp(): void + { + parent::setUp(); + Passport::actingAs($this->user()); + Log::info(sprintf('Now in %s.', get_class($this))); + } + + + /** + * @param array $submission + * + * emptyDataProvider / storeDataProvider + * + * @dataProvider storeDataProvider + */ + public function testStore(array $submission): void + { + if ([] === $submission) { + $this->markTestSkipped('Empty data provider'); + } + $route = 'api.v1.transactions.store'; + $this->storeAndCompare($route, $submission); + } + + /** + * @return array + */ + public function emptyDataProvider(): array + { + return [[[]]]; + + } + + /** + * @return array + */ + public function storeDataProvider(): array + { + $minimalSets = $this->minimalSets(); + $optionalSets = $this->optionalSets(); + $regenConfig = [ + 'title' => function () { + $faker = Factory::create(); + + return $faker->uuid; + }, + 'transactions' => [ + [ + 'description' => function () { + $faker = Factory::create(); + + return $faker->uuid; + }, + ], + ], + ]; + + return $this->genericDataProvider($minimalSets, $optionalSets, $regenConfig); + } + + /** + * @return array + */ + private function minimalSets(): array + { + $faker = Factory::create(); + + // 3 sets: + $combis = [ + ['withdrawal', 1, 8], + ['deposit', 9, 1], + ['transfer', 1, 2], + ]; + $set = []; + foreach ($combis as $combi) { + $set[] = [ + 'parameters' => [], + 'fields' => [ + // not even required but OK. + 'error_if_duplicate_hash' => $faker->boolean, + 'transactions' => [ + [ + 'type' => $combi[0], + 'date' => $faker->dateTime(null, 'Europe/Amsterdam')->format(\DateTimeInterface::RFC3339), + 'amount' => number_format($faker->randomFloat(2, 10, 100), 12), + 'description' => $faker->uuid, + 'source_id' => $combi[1], + 'destination_id' => $combi[2], + ], + ], + ], + ]; + } + + return $set; + } + + + /** + * @return \array[][] + */ + private function optionalSets(): array + { + $faker = Factory::create(); + + return [ + 'title' => [ + 'fields' => [ + 'title' => $faker->uuid, + ], + ], + 'order' => [ + 'fields' => [ + 'order' => $faker->numberBetween(1, 2), + ], + ], + 'active' => [ + 'fields' => [ + 'active' => $faker->boolean, + ], + ], + ]; + } + +} \ No newline at end of file diff --git a/tests/Traits/TestHelpers.php b/tests/Traits/TestHelpers.php index 7e8bafda37..7b0926a15d 100644 --- a/tests/Traits/TestHelpers.php +++ b/tests/Traits/TestHelpers.php @@ -43,13 +43,13 @@ trait TestHelpers { $submissions = []; /** - * @var string $name + * @var string $i * @var array $set */ - foreach ($minimalSets as $name => $set) { + foreach ($minimalSets as $i => $set) { $body = []; - foreach ($set['fields'] as $field => $value) { - $body[$field] = $value; + foreach ($set['fields'] as $ii => $value) { + $body[$ii] = $value; } // minimal set is part of all submissions: $submissions[] = [[ @@ -62,16 +62,32 @@ trait TestHelpers $optionalSets = $startOptionalSets; $keys = array_keys($optionalSets); $count = count($keys) > self::MAX_ITERATIONS ? self::MAX_ITERATIONS : count($keys); - for ($i = 1; $i <= $count; $i++) { - $combinations = $this->combinationsOf($i, $keys); + for ($iii = 1; $iii <= $count; $iii++) { + $combinations = $this->combinationsOf($iii, $keys); // expand body with N extra fields: - foreach ($combinations as $extraFields) { + foreach ($combinations as $iv => $extraFields) { $second = $body; $ignore = $set['ignore'] ?? []; // unused atm. - foreach ($extraFields as $extraField) { + foreach ($extraFields as $v => $extraField) { // now loop optional sets on $extraField and add whatever the config is: - foreach ($optionalSets[$extraField]['fields'] as $newField => $newValue) { - $second[$newField] = $newValue; + foreach ($optionalSets[$extraField]['fields'] as $vi => $newValue) { + // if the newValue is an array, we must merge it with whatever may + // or may not already be there. Its the optional field for one of the + // (maybe existing?) fields: + if (is_array($newValue) && array_key_exists($vi, $second) && is_array($second[$vi])) { + // loop $second[$vi] and merge it with whatever is in $newValue[$someIndex] + foreach ($second[$vi] as $vii => $iiValue) { + $second[$vi][$vii] = $iiValue + $newValue[$vii]; + } + } + if (!is_array($newValue)) { + $second[$vi] = $newValue; + } + } + if (array_key_exists('remove_fields', $optionalSets[$extraField])) { + foreach ($optionalSets[$extraField]['remove_fields'] as $removed) { + unset($second[$removed]); + } } } @@ -113,9 +129,20 @@ trait TestHelpers */ protected function regenerateValues($set, $opts): array { - foreach ($opts as $key => $func) { - if (array_key_exists($key, $set)) { - $set[$key] = $func(); + foreach ($opts as $i => $func) { + if (array_key_exists($i, $set)) { + if(!is_array($set[$i])) { + $set[$i] = $func(); + } + if(is_array($set[$i])) { + foreach($set[$i] as $ii => $lines) { + foreach($lines as $iii => $value) { + if(isset($opts[$i][$ii][$iii])) { + $set[$i][$ii][$iii] = $opts[$i][$ii][$iii](); + } + } + } + } } } @@ -132,7 +159,7 @@ trait TestHelpers { // get original values: $response = $this->get($route, ['Accept' => 'application/json']); - $status = $response->getStatusCode(); + $status = $response->getStatusCode(); $this->assertEquals($status, 200, sprintf(sprintf('%s failed with 404.', $route))); $response->assertStatus(200); $originalString = $response->content(); @@ -238,13 +265,49 @@ trait TestHelpers if ($this->ignoreCombination($route, $submission['type'] ?? 'blank', $returnName)) { continue; } + // check if is array, if so we need something smart: + if (is_array($returnValue) && is_array($submission[$returnName])) { + $this->compareArray($returnName, $submission[$returnName], $returnValue); + } + if (!is_array($returnValue) && !is_array($submission[$returnName])) { + $message = sprintf( + "Main: Return value '%s' of key '%s' does not match submitted value '%s'.\n%s\n%s", $returnValue, $returnName, $submission[$returnName], + json_encode($submission), $responseBody + ); + $this->assertEquals($returnValue, $submission[$returnName], $message); + } - $message = sprintf( - "Return value '%s' of key '%s' does not match submitted value '%s'.\n%s\n%s", $returnValue, $returnName, $submission[$returnName], - json_encode($submission), $responseBody - ); - $this->assertEquals($returnValue, $submission[$returnName], $message); + } + } + } + /** + * @param string $key + * @param array $original + * @param array $returned + */ + protected function compareArray(string $key, array $original, array $returned) + { + $ignore = ['id', 'created_at', 'updated_at']; + foreach ($returned as $objectKey => $object) { + // each object is a transaction, a rule trigger, a rule action, whatever. + // assume the original also contains this key: + if (!array_key_exists($objectKey, $original)) { + $message = sprintf('Sub: Original array "%s" does not have returned key %d.', $key, $objectKey); + $this->assertTrue(false, $message); + } + + foreach ($object as $returnKey => $returnValue) { + if (in_array($returnKey, $ignore, true)) { + continue; + } + if (array_key_exists($returnKey, $original[$objectKey])) { + $message = sprintf( + 'Sub: sub-array "%s" returned value %s does not match sent X value %s.', + $key, var_export($returnValue, true), var_export($original[$objectKey][$returnKey], true) + ); + $this->assertEquals($original[$objectKey][$returnKey], $returnValue, $message); + } } } }