Added condition options for events to trigger decisions
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
use App\Models\Decision;
|
||||
use App\Models\EmailTemplate;
|
||||
use App\Models\Segment;
|
||||
use App\Services\DecisionEvents\ConditionEvaluator;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
@@ -22,6 +23,8 @@ public function index(Request $request)
|
||||
'email_templates' => EmailTemplate::query()->where('active', true)->get(['id', 'name', 'entity_types']),
|
||||
'events' => \App\Models\Event::query()->orderBy('name')->get(['id', 'name', 'key', 'description', 'active']),
|
||||
'archive_settings' => ArchiveSetting::query()->where('enabled', true)->orderBy('id')->get(['id', 'name']),
|
||||
'condition_fields' => ConditionEvaluator::availableFields(),
|
||||
'condition_operators' => ConditionEvaluator::availableOperators(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,6 +86,9 @@ public function updateAction(int $id, Request $request)
|
||||
|
||||
public function storeDecision(Request $request)
|
||||
{
|
||||
$allowedConditionFields = collect(ConditionEvaluator::availableFields())->pluck('key')->implode(',');
|
||||
$allowedOperators = 'in:=,!=,>,>=,<,<=,contains';
|
||||
|
||||
$attributes = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'color_tag' => 'nullable|string|max:25',
|
||||
@@ -96,6 +102,14 @@ public function storeDecision(Request $request)
|
||||
'events.*.active' => 'sometimes|boolean',
|
||||
'events.*.run_order' => 'nullable|integer',
|
||||
'events.*.config' => 'nullable|array',
|
||||
'events.*.config.segment_id' => 'nullable|integer|exists:segments,id',
|
||||
'events.*.config.deactivate_previous' => 'sometimes|boolean',
|
||||
'events.*.config.archive_setting_id' => 'nullable|integer|exists:archive_settings,id',
|
||||
'events.*.config.reactivate' => 'sometimes|boolean',
|
||||
'events.*.config.conditions' => 'nullable|array',
|
||||
'events.*.config.conditions.*.field' => "required_with:events.*.config.conditions.*|string|in:{$allowedConditionFields}",
|
||||
'events.*.config.conditions.*.operator' => "required_with:events.*.config.conditions.*|string|{$allowedOperators}",
|
||||
'events.*.config.conditions.*.value' => 'required_with:events.*.config.conditions.*',
|
||||
]);
|
||||
|
||||
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
||||
@@ -112,12 +126,12 @@ public function storeDecision(Request $request)
|
||||
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
||||
if ($key === 'add_segment') {
|
||||
$seg = $ev['config']['segment_id'] ?? null;
|
||||
if (empty($seg) || ! Segment::where('id', $seg)->exists()) {
|
||||
if (empty($seg)) {
|
||||
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
||||
}
|
||||
} elseif ($key === 'archive_contract') {
|
||||
$as = $ev['config']['archive_setting_id'] ?? null;
|
||||
if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) {
|
||||
if (empty($as)) {
|
||||
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
||||
}
|
||||
}
|
||||
@@ -174,6 +188,9 @@ public function updateDecision(int $id, Request $request)
|
||||
{
|
||||
$row = Decision::findOrFail($id);
|
||||
|
||||
$allowedConditionFields = collect(ConditionEvaluator::availableFields())->pluck('key')->implode(',');
|
||||
$allowedOperators = 'in:=,!=,>,>=,<,<=,contains';
|
||||
|
||||
$attributes = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'color_tag' => 'nullable|string|max:25',
|
||||
@@ -187,6 +204,14 @@ public function updateDecision(int $id, Request $request)
|
||||
'events.*.active' => 'sometimes|boolean',
|
||||
'events.*.run_order' => 'nullable|integer',
|
||||
'events.*.config' => 'nullable|array',
|
||||
'events.*.config.segment_id' => 'nullable|integer|exists:segments,id',
|
||||
'events.*.config.deactivate_previous' => 'sometimes|boolean',
|
||||
'events.*.config.archive_setting_id' => 'nullable|integer|exists:archive_settings,id',
|
||||
'events.*.config.reactivate' => 'sometimes|boolean',
|
||||
'events.*.config.conditions' => 'nullable|array',
|
||||
'events.*.config.conditions.*.field' => "required_with:events.*.config.conditions.*|string|in:{$allowedConditionFields}",
|
||||
'events.*.config.conditions.*.operator' => "required_with:events.*.config.conditions.*|string|{$allowedOperators}",
|
||||
'events.*.config.conditions.*.value' => 'required_with:events.*.config.conditions.*',
|
||||
]);
|
||||
|
||||
$actionIds = collect($attributes['actions'] ?? [])->pluck('id')->toArray();
|
||||
@@ -203,12 +228,12 @@ public function updateDecision(int $id, Request $request)
|
||||
$key = $eventModel?->key ?? ($ev['key'] ?? null);
|
||||
if ($key === 'add_segment') {
|
||||
$seg = $ev['config']['segment_id'] ?? null;
|
||||
if (empty($seg) || ! Segment::where('id', $seg)->exists()) {
|
||||
if (empty($seg)) {
|
||||
$validationErrors["events.$i.config.segment_id"] = 'Please select a valid segment for the add_segment event.';
|
||||
}
|
||||
} elseif ($key === 'archive_contract') {
|
||||
$as = $ev['config']['archive_setting_id'] ?? null;
|
||||
if (empty($as) || ! ArchiveSetting::where('id', $as)->exists()) {
|
||||
if (empty($as)) {
|
||||
$validationErrors["events.$i.config.archive_setting_id"] = 'Please select a valid archive setting for the archive_contract event.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Models\Event as DecisionEventModel;
|
||||
use App\Services\DecisionEvents\ConditionEvaluator;
|
||||
use App\Services\DecisionEvents\DecisionEventContext;
|
||||
use App\Services\DecisionEvents\Registry;
|
||||
use Illuminate\Bus\Queueable;
|
||||
@@ -68,6 +69,23 @@ public function handle(): void
|
||||
user: $activity->user,
|
||||
);
|
||||
|
||||
// [2] Condition check — skip the event if any condition is not met
|
||||
$conditions = $this->config['conditions'] ?? [];
|
||||
if (! empty($conditions)) {
|
||||
$conditionsMet = app(ConditionEvaluator::class)->evaluate($conditions, $context);
|
||||
if (! $conditionsMet) {
|
||||
DB::table('decision_event_logs')->where('idempotency_key', $idempotencyKey)->update([
|
||||
'status' => 'skipped',
|
||||
'message' => 'Condition not met',
|
||||
'finished_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// [3] Resolve handler → handle()
|
||||
$handler->handle($context, $this->config);
|
||||
|
||||
DB::table('decision_event_logs')->where('idempotency_key', $idempotencyKey)->update([
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\DecisionEvents;
|
||||
|
||||
class ConditionEvaluator
|
||||
{
|
||||
/**
|
||||
* Returns true when ALL conditions pass (AND logic).
|
||||
*
|
||||
* Each condition: { field: string, operator: string, value: mixed }
|
||||
*
|
||||
* @param array<int, array{field: string, operator: string, value: mixed}> $conditions
|
||||
*/
|
||||
public function evaluate(array $conditions, DecisionEventContext $context): bool
|
||||
{
|
||||
foreach ($conditions as $condition) {
|
||||
if (! $this->evaluateOne($condition, $context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function evaluateOne(array $condition, DecisionEventContext $context): bool
|
||||
{
|
||||
$field = $condition['field'] ?? '';
|
||||
$operator = $condition['operator'] ?? '=';
|
||||
$expected = $condition['value'] ?? null;
|
||||
|
||||
$actual = $this->resolveField($field, $context);
|
||||
|
||||
return $this->compare($actual, $operator, $expected);
|
||||
}
|
||||
|
||||
protected function resolveField(string $field, DecisionEventContext $context): mixed
|
||||
{
|
||||
return match ($field) {
|
||||
'activity.amount' => $context->activity?->amount,
|
||||
'activity.note' => $context->activity?->note,
|
||||
'contract.active' => $context->contract !== null ? (bool) $context->contract->active : null,
|
||||
'contract.account.balance_amount' => $this->resolveAccountBalance($context),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveAccountBalance(DecisionEventContext $context): mixed
|
||||
{
|
||||
if (! $context->contract) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$context->contract->loadMissing('account');
|
||||
|
||||
return $context->contract->account?->balance_amount;
|
||||
}
|
||||
|
||||
protected function compare(mixed $actual, string $operator, mixed $expected): bool
|
||||
{
|
||||
if ($actual === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($operator, ['>', '>=', '<', '<='], true)) {
|
||||
$actual = (float) $actual;
|
||||
$expected = (float) $expected;
|
||||
}
|
||||
|
||||
return match ($operator) {
|
||||
'=' => $actual == $expected,
|
||||
'!=' => $actual != $expected,
|
||||
'>' => $actual > $expected,
|
||||
'>=' => $actual >= $expected,
|
||||
'<' => $actual < $expected,
|
||||
'<=' => $actual <= $expected,
|
||||
'contains' => str_contains((string) $actual, (string) $expected),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns available condition field definitions for the frontend.
|
||||
*
|
||||
* @return array<int, array{key: string, label: string, type: string}>
|
||||
*/
|
||||
public static function availableFields(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'activity.amount', 'label' => 'Aktivnost – znesek', 'type' => 'numeric'],
|
||||
['key' => 'activity.note', 'label' => 'Aktivnost – opomba', 'type' => 'string'],
|
||||
['key' => 'contract.active', 'label' => 'Pogodba – aktivna', 'type' => 'boolean'],
|
||||
['key' => 'contract.account.balance_amount', 'label' => 'Račun – stanje', 'type' => 'numeric'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns available operators grouped by field type.
|
||||
*
|
||||
* @return array<string, array<int, array{key: string, label: string}>>
|
||||
*/
|
||||
public static function availableOperators(): array
|
||||
{
|
||||
return [
|
||||
'numeric' => [
|
||||
['key' => '=', 'label' => 'je enako'],
|
||||
['key' => '!=', 'label' => 'ni enako'],
|
||||
['key' => '>', 'label' => 'je večje od'],
|
||||
['key' => '>=', 'label' => 'je večje ali enako'],
|
||||
['key' => '<', 'label' => 'je manjše od'],
|
||||
['key' => '<=', 'label' => 'je manjše ali enako'],
|
||||
],
|
||||
'string' => [
|
||||
['key' => '=', 'label' => 'je enako'],
|
||||
['key' => '!=', 'label' => 'ni enako'],
|
||||
['key' => 'contains', 'label' => 'vsebuje'],
|
||||
],
|
||||
'boolean' => [
|
||||
['key' => '=', 'label' => 'je'],
|
||||
['key' => '!=', 'label' => 'ni'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user