mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Fix tests for transaction storage.
This commit is contained in:
parent
288052905e
commit
beece4dcbb
@ -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);
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'] ?? []);
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
205
tests/Api/Models/Recurrence/StoreControllerTest.php
Normal file
205
tests/Api/Models/Recurrence/StoreControllerTest.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
/*
|
||||
* StoreControllerTest.php
|
||||
* Copyright (c) 2021 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* 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 (at your option) 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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),
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
237
tests/Api/Models/Rule/StoreControllerTest.php
Normal file
237
tests/Api/Models/Rule/StoreControllerTest.php
Normal file
@ -0,0 +1,237 @@
|
||||
<?php
|
||||
/*
|
||||
* StoreControllerTest.php
|
||||
* Copyright (c) 2021 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* 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 (at your option) 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
167
tests/Api/Models/Transaction/StoreControllerTest.php
Normal file
167
tests/Api/Models/Transaction/StoreControllerTest.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/*
|
||||
* StoreControllerTest.php
|
||||
* Copyright (c) 2021 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* 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 (at your option) 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user