Files
sure/app/models/rule/condition.rb
Juan José Mata e5ed946959 Add rules import/export support (#424)
* Add full import/export support for rules with versioned JSON schema

This commit implements comprehensive import/export functionality for rules,
allowing users to back up and restore their rule definitions.

Key features:
- Export rules to both CSV and NDJSON formats with versioned schema (v1)
- Import rules from CSV with full support for nested conditions and actions
- UUID to name mapping for categories and merchants for portability
- Support for compound conditions with sub-conditions
- Comprehensive test coverage for export and import functionality
- UI integration for rules import in the imports interface

Technical details:
- Extended Family::DataExporter to generate rules.csv and include rules in all.ndjson
- Created RuleImport model following the existing Import STI pattern
- Added migration for rule-specific columns in import_rows table
- Implemented serialization helpers to map UUIDs to human-readable names
- Added i18n support for the new import option
- Included versioning in NDJSON export to support future schema evolution

The implementation ensures rules can be safely exported from one family
and imported into another, even when category/merchant IDs differ,
by mapping between names and IDs during export/import.

* Fix AR migration version

* Mention support for rules export

* Rabbit suggestion

* Fix tests

* Missed schema.rb

* Fix sample CSV download for rule import

* Fix parsing in Rules import

* Fix tests

* Rule import message i18n

* Export tag names, not UUIDs

* Make sure tags are created if needed at import

* Avoid test errors when running in parallel

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-07 13:20:54 +01:00

75 lines
1.7 KiB
Ruby

class Rule::Condition < ApplicationRecord
belongs_to :rule, touch: true, optional: -> { where.not(parent_id: nil) }
belongs_to :parent, class_name: "Rule::Condition", optional: true, inverse_of: :sub_conditions
has_many :sub_conditions, -> { order(:created_at, :id) }, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy, inverse_of: :parent
validates :condition_type, presence: true
validates :operator, presence: true
validates :value, presence: true, unless: -> { compound? || operator == "is_null" }
accepts_nested_attributes_for :sub_conditions, allow_destroy: true
# We don't store rule_id on sub_conditions, so "walk up" to the parent rule
def rule
parent&.rule || super
end
def compound?
condition_type == "compound"
end
def apply(scope)
if compound?
build_compound_scope(scope)
else
filter.apply(scope, operator, value)
end
end
def prepare(scope)
if compound?
sub_conditions.reduce(scope) { |s, sub| sub.prepare(s) }
else
filter.prepare(scope)
end
end
def value_display
if value.present?
if options
options.find { |option| option.last == value }&.first
else
value
end
else
""
end
end
def options
filter.options
end
def operators
filter.operators
end
def filter
rule.registry.get_filter!(condition_type)
end
private
def build_compound_scope(scope)
if operator == "or"
combined_scope = sub_conditions
.map { |sub| sub.apply(scope) }
.reduce { |acc, s| acc.or(s) }
combined_scope || scope
else
sub_conditions.reduce(scope) { |s, sub| sub.apply(s) }
end
end
end