Skip to content

feat(exports): add rule operand references#1726

Merged
jjmata merged 11 commits into
we-promise:mainfrom
JSONbored:codex/test-rule-operand-roundtrip
May 12, 2026
Merged

feat(exports): add rule operand references#1726
jjmata merged 11 commits into
we-promise:mainfrom
JSONbored:codex/test-rule-operand-roundtrip

Conversation

@JSONbored
Copy link
Copy Markdown
Contributor

@JSONbored JSONbored commented May 10, 2026

Summary

  • Adds normalized operand references to exported rule conditions and actions.
  • Keeps existing portable rule values intact while adding stable metadata for machine comparison.
  • Allows rule import to recover category, merchant, and tag operands from reference names when raw IDs are not portable.

What changed

  • Rule NDJSON export now includes value_ref objects for category, merchant, and tag operands when the source record can be resolved.
  • Rule import now falls back to value_ref.name when value is absent.
  • Tests cover category condition refs, category action refs, tag action refs, and import from normalized operand refs.

Why

Names alone are not enough for adapter-grade parity, and raw UUIDs are not portable across restored families. Exporting normalized operand references gives clients a machine-comparable contract without breaking existing import behavior.

Validation

  • docker compose -f .devcontainer/docker-compose.yml exec -T app bash -lc 'cd /workspace && bin/rails test test/models/family/data_exporter_test.rb test/models/family/data_importer_test.rb'
  • docker compose -f .devcontainer/docker-compose.yml exec -T app bash -lc 'cd /workspace && bin/rubocop app/models/family/data_exporter.rb app/models/family/data_importer.rb test/models/family/data_exporter_test.rb test/models/family/data_importer_test.rb'
  • git diff --check

Notes

  • Draft PR for focused review.
  • Existing value fields remain present in exports; value_ref is additive metadata.

Summary by CodeRabbit

  • New Features

    • Exports now include structured value_ref metadata (type/id/name) alongside human-readable operand values for rules.
    • Imports now honor value_ref payloads to resolve references and avoid creating duplicates from stale UUID-like legacy values.
    • Centralized UUID-like detection to prevent incorrect name fallbacks during import.
  • Tests

    • Added and updated tests covering export of value_refs and import behavior with legacy UUID-like operands.
  • Chores

    • Added a shared UUID validation utility.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Family::DataExporter emits structured value_ref objects (type/id/name) alongside human-readable operand value. Family::DataImporter uses rule_operand_value to prefer data["value"] but will use value_ref.name (especially when value is UUID-like) to resolve and persist record IDs.

Changes

Rule Value Reference Serialization

Layer / File(s) Summary
UUID detection constants
lib/uuid_format.rb, test/lib/uuid_format_test.rb
Adds UuidFormat with PATTERN and valid?, plus tests for accepted/rejected UUID-like values.
Controller UUID validation
app/controllers/api/v1/base_controller.rb, app/controllers/api/v1/valuations_controller.rb
Removes controller-level UUID regex; BaseController#valid_uuid? now uses UuidFormat.valid?; duplicate valid_uuid? removed from ValuationsController.
Export Entry Points
app/models/family/data_exporter.rb
serialize_condition and serialize_action derive data[:value] from a resolved operand and include data[:value_ref] when a record is resolvable.
Export Mapping Helpers
app/models/family/data_exporter.rb
Adds resolve_condition_operand, resolve_action_operand, rule_operand, resolve_rule_operand_record, rule_value_ref, and uuid_like?; removes old resolve_condition_value/resolve_action_value.
Import Entry Points
app/models/family/data_importer.rb
resolve_rule_condition_value and resolve_rule_action_value now call rule_operand_value(data) rather than using data["value"] directly.
Import Operand Extraction
app/models/family/data_importer.rb
rule_operand_value(data) prefers data["value"], but if value is UUID-like and value_ref.name exists returns value_ref.name; otherwise falls back to value_ref.name when value is missing.
Export Tests
test/models/family/data_exporter_test.rb
Extended and new tests assert exported NDJSON includes value_ref objects for mapped operands and that stale UUIDs do not trigger name fallback.
Import Tests
test/models/family/data_importer_test.rb
New tests verify importer resolves value_ref into persisted Merchant/Category IDs and ignores stale UUID legacy values without creating incorrect records.

Sequence Diagram

sequenceDiagram
  participant Exporter as Family::DataExporter
  participant Resolver as resolve_rule_operand_record / rule_operand
  participant Store as Category/Merchant/Tag
  participant Exported as Exported Rule NDJSON
  participant Importer as Family::DataImporter
  participant OperandPicker as rule_operand_value

  Exporter->>Resolver: request operand for condition/action (input value)
  Resolver->>Store: lookup by id (if uuid_like?) or by name
  Store-->>Resolver: return record (id, name, type) or nil
  Resolver-->>Exporter: return { value, value_ref? }
  Exporter->>Exported: emit value + optional value_ref (type/id/name)

  Importer->>OperandPicker: supply exported condition/action data
  OperandPicker-->>Importer: return operand (value or value_ref.name)
  Importer->>Store: resolve operand to record ID
  Store-->>Importer: return ID
  Importer->>Importer: persist rule with resolved ID
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Suggested labels

contributor:flagged

Suggested reviewers

  • jjmata
  • sure-admin

Poem

🐰 A rabbit nibbles through the code at night,
Value refs now sparkle, tidy and bright.
UUIDs bow, names lead the way,
Exported, imported—rules safely play. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(exports): add rule operand references' clearly and concisely summarizes the main change: adding value_ref operand references to rule exports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Collaborator

jjmata commented May 11, 2026

The value_ref sidecar approach (export carries both the raw UUID value and a {type, id, name} reference object; importer falls back to value_ref.name when the UUID doesn't resolve) is a clean forward-compatibility design.

One thing in resolve_action_value_ref (same pattern appears in resolve_condition_value_ref):

category = @family.categories.find_by(id: action.value) || @family.categories.find_by(name: action.value)

When action.value is a UUID that no longer exists (deleted category, or an export from another family), the ID lookup returns nil and a second DB query runs trying to find a category named with that UUID string. It won't match, but it's a redundant round-trip on every export pass for every such rule. A quick valid_uuid?(action.value) guard (the helper already exists on the controller) before the name-fallback would avoid the extra query without changing behaviour.


Generated by Claude Code

@JSONbored JSONbored marked this pull request as ready for review May 11, 2026 06:17
@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/models/family/data_exporter.rb (1)

434-450: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve each operand record only once.

These value_ref additions re-query the same category/merchant/tag record that resolve_condition_value / resolve_action_value already looked up, so rule export now adds 2-4 queries per mapped operand. On larger exports this becomes an avoidable N+1 path. Please derive both value and value_ref from a single resolved record; at minimum, skip the name fallback when the raw value already looks like an unresolved UUID.

As per coding guidelines, app/models/**/*.rb: Optimize database access with proper indexes and prevent N+1 queries via includes/joins.

Also applies to: 473-485, 511-528

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/family/data_exporter.rb` around lines 434 - 450, The serialize
methods (serialize_condition and serialize_action) currently call
resolve_*_value and then separately call resolve_*_value_ref which re-queries
the same referenced record; change them to resolve the operand once and derive
both value and value_ref from that single resolved record by: (1) add/modify a
helper that returns the resolved record (category/merchant/tag) plus the
formatted value, (2) have serialize_condition use that helper to set both
data[:value] and data[:value_ref] without extra DB lookups, and (3) when the raw
stored value already matches an UUID format, skip the name-fallback lookup to
avoid an unnecessary query. Apply the same refactor to the other occurrences
noted (the blocks around lines 473-485 and 511-528) so all value/value_ref pairs
come from one resolved object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/family/data_importer.rb`:
- Around line 691-693: The current rule_operand_value method returns
data["value"] even when it contains a non-portable legacy UUID; change it to
prefer data.dig("value_ref","name") when data["value"] is a raw UUID (i.e.
matches the UUID hex-with-hyphens 36-char format) and otherwise keep the
existing behavior (use data["value"] if present, else value_ref.name). Update
rule_operand_value to detect UUID-like values and fall back to value_ref.name in
that case; keep references to data["value"] and data.dig("value_ref","name") in
the implementation.

---

Outside diff comments:
In `@app/models/family/data_exporter.rb`:
- Around line 434-450: The serialize methods (serialize_condition and
serialize_action) currently call resolve_*_value and then separately call
resolve_*_value_ref which re-queries the same referenced record; change them to
resolve the operand once and derive both value and value_ref from that single
resolved record by: (1) add/modify a helper that returns the resolved record
(category/merchant/tag) plus the formatted value, (2) have serialize_condition
use that helper to set both data[:value] and data[:value_ref] without extra DB
lookups, and (3) when the raw stored value already matches an UUID format, skip
the name-fallback lookup to avoid an unnecessary query. Apply the same refactor
to the other occurrences noted (the blocks around lines 473-485 and 511-528) so
all value/value_ref pairs come from one resolved object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd5a93be-f080-41bb-9a68-cb9f9bacd138

📥 Commits

Reviewing files that changed from the base of the PR and between 36960fe and 36b3271.

📒 Files selected for processing (4)
  • app/models/family/data_exporter.rb
  • app/models/family/data_importer.rb
  • test/models/family/data_exporter_test.rb
  • test/models/family/data_importer_test.rb

Comment thread app/models/family/data_importer.rb
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/models/family/data_exporter.rb (2)

475-494: 💤 Low value

Consider removing redundant presence checks.

Lines 479, 484, and 489 check action.value.present? again after the early return on line 476 already ensured value is present. While harmless, these checks are redundant.

♻️ Optional simplification
 def resolve_action_operand(action)
   return rule_operand(action.value) unless action.value.present?

   # Map category UUIDs to names for portability
-  if action.action_type == "set_transaction_category" && action.value.present?
+  if action.action_type == "set_transaction_category"
     return rule_operand(action.value, type: "Category", relation: `@family.categories`, fallback_to_name: true)
   end

   # Map merchant UUIDs to names for portability
-  if action.action_type == "set_transaction_merchant" && action.value.present?
+  if action.action_type == "set_transaction_merchant"
     return rule_operand(action.value, type: "Merchant", relation: `@family.merchants`, fallback_to_name: true)
   end

   # Map tag UUIDs to names for portability
-  if action.action_type == "set_transaction_tags" && action.value.present?
+  if action.action_type == "set_transaction_tags"
     return rule_operand(action.value, type: "Tag", relation: `@family.tags`, fallback_to_name: true)
   end

   rule_operand(action.value)
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/family/data_exporter.rb` around lines 475 - 494, The method
resolve_action_operand redundantly re-checks action.value.present? inside three
branches after the early guard return; remove the extra presence checks so each
branch only tests action.action_type and calls rule_operand with the same params
(e.g., for "set_transaction_category" call rule_operand(action.value, type:
"Category", relation: `@family.categories`, fallback_to_name: true), similarly for
"set_transaction_merchant" and "set_transaction_tags"), leaving the initial
return rule_operand(action.value) unless action.value.present? guard intact.

459-473: 💤 Low value

Consider removing redundant presence checks.

Lines 463 and 468 check condition.value.present? again after the early return on line 460 already ensured value is present. While harmless, these checks are redundant.

♻️ Optional simplification
 def resolve_condition_operand(condition)
   return rule_operand(condition.value) unless condition.value.present?

   # Map category UUIDs to names for portability
-  if condition.condition_type == "transaction_category" && condition.value.present?
+  if condition.condition_type == "transaction_category"
     return rule_operand(condition.value, type: "Category", relation: `@family.categories`)
   end

   # Map merchant UUIDs to names for portability
-  if condition.condition_type == "transaction_merchant" && condition.value.present?
+  if condition.condition_type == "transaction_merchant"
     return rule_operand(condition.value, type: "Merchant", relation: `@family.merchants`)
   end

   rule_operand(condition.value)
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/family/data_exporter.rb` around lines 459 - 473, The method
resolve_condition_operand redundantly checks condition.value.present? inside the
transaction_category and transaction_merchant branches even though there's an
early return for missing value; remove the repeated presence checks so the
branches simply inspect condition.condition_type and call
rule_operand(condition.value, type: "Category", relation: `@family.categories`) or
rule_operand(condition.value, type: "Merchant", relation: `@family.merchants`) as
appropriate, leaving the initial guard (return rule_operand(condition.value)
unless condition.value.present?) and the final fallback call to
rule_operand(condition.value) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/models/family/data_exporter.rb`:
- Around line 475-494: The method resolve_action_operand redundantly re-checks
action.value.present? inside three branches after the early guard return; remove
the extra presence checks so each branch only tests action.action_type and calls
rule_operand with the same params (e.g., for "set_transaction_category" call
rule_operand(action.value, type: "Category", relation: `@family.categories`,
fallback_to_name: true), similarly for "set_transaction_merchant" and
"set_transaction_tags"), leaving the initial return rule_operand(action.value)
unless action.value.present? guard intact.
- Around line 459-473: The method resolve_condition_operand redundantly checks
condition.value.present? inside the transaction_category and
transaction_merchant branches even though there's an early return for missing
value; remove the repeated presence checks so the branches simply inspect
condition.condition_type and call rule_operand(condition.value, type:
"Category", relation: `@family.categories`) or rule_operand(condition.value, type:
"Merchant", relation: `@family.merchants`) as appropriate, leaving the initial
guard (return rule_operand(condition.value) unless condition.value.present?) and
the final fallback call to rule_operand(condition.value) intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0cb1c7bd-4f82-48d5-908e-220af541139d

📥 Commits

Reviewing files that changed from the base of the PR and between 36b3271 and 3314017.

📒 Files selected for processing (4)
  • app/models/family/data_exporter.rb
  • app/models/family/data_importer.rb
  • test/models/family/data_exporter_test.rb
  • test/models/family/data_importer_test.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/models/family/data_importer.rb
  • test/models/family/data_exporter_test.rb

Copy link
Copy Markdown
Collaborator

@jjmata jjmata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the UUID_FORMATs be imported from somewhere else?

Comment thread app/models/family/data_exporter.rb Outdated
Comment thread app/models/family/data_importer.rb Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/family/data_importer.rb`:
- Around line 691-698: In rule_operand_value, avoid using .presence directly on
data["value"] (which treats false as blank); instead preserve explicit
false/non-string values by first extracting raw = data["value"], then normalize
only strings with raw.is_a?(String) ? raw.presence : raw to produce value;
change the uuid fallback check to only run for string values (e.g. if
value.is_a?(String) && uuid_like?(value) && value_ref_name.present? then return
value_ref_name), and finally return value unless it is nil (fall back to
value_ref_name) so boolean false is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e253969b-6ef7-4503-977e-c2f89c2eb185

📥 Commits

Reviewing files that changed from the base of the PR and between 6b64d1d and eb28411.

📒 Files selected for processing (6)
  • app/controllers/api/v1/base_controller.rb
  • app/controllers/api/v1/valuations_controller.rb
  • app/models/family/data_exporter.rb
  • app/models/family/data_importer.rb
  • lib/uuid_format.rb
  • test/lib/uuid_format_test.rb
💤 Files with no reviewable changes (1)
  • app/controllers/api/v1/valuations_controller.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/family/data_exporter.rb

Comment thread app/models/family/data_importer.rb
@JSONbored JSONbored requested a review from jjmata May 12, 2026 07:11
@jjmata jjmata merged commit 0ab3b0b into we-promise:main May 12, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

2 participants