diff --git a/resources/js/components/automation-rules/create-automation-rule-dialog.tsx b/resources/js/components/automation-rules/create-automation-rule-dialog.tsx index 43181ba7..ac172255 100644 --- a/resources/js/components/automation-rules/create-automation-rule-dialog.tsx +++ b/resources/js/components/automation-rules/create-automation-rule-dialog.tsx @@ -202,7 +202,7 @@ export function CreateAutomationRuleDialog({ id="rules_json" value={rulesJson} onChange={(e) => setRulesJson(e.target.value)} - placeholder='{"==": [{"var": "description"}, "GROCERY"]}' + placeholder='{"in": ["grocery", {"var": "description"}]}' rows={4} className="font-mono text-sm" required @@ -230,18 +230,24 @@ export function CreateAutomationRuleDialog({
Available fields: +

+ Note: Use any case for description and notes - matching is automatic! +

Example rules:
-{`{"in": ["GROCERY", {"var": "description"}]}
+{`{"in": ["grocery", {"var": "description"}]}
+{"in": ["M3 SPORT", {"var": "description"}]}
+{"in": ["important", {"var": "notes"}]}
 {"and": [
   {">": [{"var": "amount"}, 100]},
   {"==": [{"var": "bank_name"}, "Chase"]}
diff --git a/resources/js/components/automation-rules/edit-automation-rule-dialog.tsx b/resources/js/components/automation-rules/edit-automation-rule-dialog.tsx
index e2cc32b8..587c3026 100644
--- a/resources/js/components/automation-rules/edit-automation-rule-dialog.tsx
+++ b/resources/js/components/automation-rules/edit-automation-rule-dialog.tsx
@@ -229,7 +229,7 @@ export function EditAutomationRuleDialog({
                             id="rules_json"
                             value={rulesJson}
                             onChange={(e) => setRulesJson(e.target.value)}
-                            placeholder='{"==": [{"var": "description"}, "GROCERY"]}'
+                            placeholder='{"in": ["grocery", {"var": "description"}]}'
                             rows={4}
                             className="font-mono text-sm"
                             required
@@ -257,18 +257,24 @@ export function EditAutomationRuleDialog({
                             
Available fields:
    -
  • description (string)
  • +
  • description (string, case-insensitive)
  • +
  • notes (string or null, case-insensitive)
  • amount (number)
  • transaction_date (string)
  • bank_name (string)
  • account_name (string)
  • category (string or null)
+

+ Note: Use any case for description and notes - matching is automatic! +

Example rules:
-{`{"in": ["GROCERY", {"var": "description"}]}
+{`{"in": ["grocery", {"var": "description"}]}
+{"in": ["M3 SPORT", {"var": "description"}]}
+{"in": ["important", {"var": "notes"}]}
 {"and": [
   {">": [{"var": "amount"}, 100]},
   {"==": [{"var": "bank_name"}, "Chase"]}
diff --git a/resources/js/lib/rule-engine.ts b/resources/js/lib/rule-engine.ts
index 3f8fb9f3..727222bc 100644
--- a/resources/js/lib/rule-engine.ts
+++ b/resources/js/lib/rule-engine.ts
@@ -19,6 +19,40 @@ export interface TransactionData {
     bank_name: string;
     account_name: string;
     category: string | null;
+    notes: string | null;
+}
+
+function normalizeRuleJson(rulesJson: unknown): unknown {
+    if (typeof rulesJson === 'string') {
+        return rulesJson.toLowerCase();
+    }
+
+    if (Array.isArray(rulesJson)) {
+        return rulesJson.map((item, index) => {
+            if (index === 0 && typeof item === 'string') {
+                return item.toLowerCase();
+            }
+            if (
+                typeof item === 'object' &&
+                item !== null &&
+                'var' in item &&
+                (item.var === 'description' || item.var === 'notes')
+            ) {
+                return item;
+            }
+            return normalizeRuleJson(item);
+        });
+    }
+
+    if (typeof rulesJson === 'object' && rulesJson !== null) {
+        const normalized: Record = {};
+        for (const [key, value] of Object.entries(rulesJson)) {
+            normalized[key] = normalizeRuleJson(value);
+        }
+        return normalized;
+    }
+
+    return rulesJson;
 }
 
 export function prepareTransactionData(
@@ -36,12 +70,13 @@ export function prepareTransactionData(
         : null;
 
     return {
-        description: transaction.decryptedDescription || '',
+        description: (transaction.decryptedDescription || '').toLowerCase(),
         amount: parseFloat(transaction.amount),
         transaction_date: transaction.transaction_date,
         bank_name: bank?.name || '',
         account_name: account?.name || '',
         category: category?.name || null,
+        notes: transaction.decryptedNotes?.toLowerCase() || null,
     };
 }
 
@@ -71,7 +106,10 @@ export function evaluateRules(
             );
             consoleDebug('[Rule Engine] Rule JSON:', rule.rules_json);
 
-            const result = jsonLogic.apply(rule.rules_json, transactionData);
+            const normalizedRulesJson = normalizeRuleJson(rule.rules_json);
+            consoleDebug('[Rule Engine] Normalized Rule JSON:', normalizedRulesJson);
+
+            const result = jsonLogic.apply(normalizedRulesJson, transactionData);
 
             consoleDebug(`[Rule Engine] Rule #${rule.id} result:`, result);
 
diff --git a/tests/Feature/AutomationRuleTest.php b/tests/Feature/AutomationRuleTest.php
index 853b8e47..e8951f2f 100644
--- a/tests/Feature/AutomationRuleTest.php
+++ b/tests/Feature/AutomationRuleTest.php
@@ -24,7 +24,7 @@ test('user can create an automation rule with category action', function () {
     $response = $this->actingAs($user)->post(route('automation-rules.store'), [
         'title' => 'Grocery Rule',
         'priority' => 10,
-        'rules_json' => json_encode(['in' => ['GROCERY', ['var' => 'description']]]),
+        'rules_json' => json_encode(['in' => ['grocery', ['var' => 'description']]]),
         'action_category_id' => $category->id,
         'action_note' => null,
         'action_note_iv' => null,
@@ -203,3 +203,83 @@ test('user cannot use another users category in rule', function () {
 
     $response->assertSessionHasErrors('action_category_id');
 });
+
+test('rules with description filter are case insensitive with lowercase rule', function () {
+    $user = User::factory()->create();
+    $category = Category::factory()->create(['user_id' => $user->id]);
+
+    $response = $this->actingAs($user)->post(route('automation-rules.store'), [
+        'title' => 'Case Insensitive Description Rule (lowercase)',
+        'priority' => 10,
+        'rules_json' => json_encode(['in' => ['m3 sport', ['var' => 'description']]]),
+        'action_category_id' => $category->id,
+        'action_note' => null,
+        'action_note_iv' => null,
+    ]);
+
+    $response->assertRedirect(route('automation-rules.index'));
+    $this->assertDatabaseHas('automation_rules', [
+        'user_id' => $user->id,
+        'title' => 'Case Insensitive Description Rule (lowercase)',
+    ]);
+});
+
+test('rules with description filter are case insensitive with uppercase rule', function () {
+    $user = User::factory()->create();
+    $category = Category::factory()->create(['user_id' => $user->id]);
+
+    $response = $this->actingAs($user)->post(route('automation-rules.store'), [
+        'title' => 'Case Insensitive Description Rule (uppercase)',
+        'priority' => 10,
+        'rules_json' => json_encode(['in' => ['M3 SPORT', ['var' => 'description']]]),
+        'action_category_id' => $category->id,
+        'action_note' => null,
+        'action_note_iv' => null,
+    ]);
+
+    $response->assertRedirect(route('automation-rules.index'));
+    $this->assertDatabaseHas('automation_rules', [
+        'user_id' => $user->id,
+        'title' => 'Case Insensitive Description Rule (uppercase)',
+    ]);
+});
+
+test('rules with description filter are case insensitive with mixed case rule', function () {
+    $user = User::factory()->create();
+    $category = Category::factory()->create(['user_id' => $user->id]);
+
+    $response = $this->actingAs($user)->post(route('automation-rules.store'), [
+        'title' => 'Case Insensitive Description Rule (mixed)',
+        'priority' => 10,
+        'rules_json' => json_encode(['in' => ['M3 Sport Academy', ['var' => 'description']]]),
+        'action_category_id' => $category->id,
+        'action_note' => null,
+        'action_note_iv' => null,
+    ]);
+
+    $response->assertRedirect(route('automation-rules.index'));
+    $this->assertDatabaseHas('automation_rules', [
+        'user_id' => $user->id,
+        'title' => 'Case Insensitive Description Rule (mixed)',
+    ]);
+});
+
+test('rules with notes filter are case insensitive', function () {
+    $user = User::factory()->create();
+    $category = Category::factory()->create(['user_id' => $user->id]);
+
+    $response = $this->actingAs($user)->post(route('automation-rules.store'), [
+        'title' => 'Case Insensitive Notes Rule',
+        'priority' => 10,
+        'rules_json' => json_encode(['in' => ['IMPORTANT NOTE', ['var' => 'notes']]]),
+        'action_category_id' => $category->id,
+        'action_note' => null,
+        'action_note_iv' => null,
+    ]);
+
+    $response->assertRedirect(route('automation-rules.index'));
+    $this->assertDatabaseHas('automation_rules', [
+        'user_id' => $user->id,
+        'title' => 'Case Insensitive Notes Rule',
+    ]);
+});