Skip to content

fix(import): tolerate null Rule names and orphaned rejected transfers#2775

Open
pro3958 wants to merge 3 commits into
we-promise:mainfrom
pro3958:fix/issue-2721
Open

fix(import): tolerate null Rule names and orphaned rejected transfers#2775
pro3958 wants to merge 3 commits into
we-promise:mainfrom
pro3958:fix/issue-2721

Conversation

@pro3958

@pro3958 pro3958 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #2721

Problem

Importing a full all.ndjson export through the Raw Data / Full .ndjson import
flow aborted on exports that are perfectly valid data, for two reasons:

  1. Rule.name was treated as required. The Sure preflight listed name as a
    required field for Rule, but rules.name is nullable (db/schema.rb) and the
    model explicitly allows it (validates :name, length: { minimum: 1 }, allow_nil: true).
    A single rule with "name": null failed preflight and blocked the entire import.

  2. Dangling RejectedTransfer rows aborted the import. A RejectedTransfer is
    an advisory hint that stops two transactions from being re-matched. When one of
    the referenced transactions is gone (e.g. deleted after the rejection), the
    preflight raised a hard missing_reference error, and — even past preflight — the
    importer raised MissingReferenceError in strict (session) mode instead of taking
    the next unless inflow && outflow skip path that was already written for it.

Fix

  • SureImport::Preflight: require Rule.id (the field the importer actually needs,
    consistent with every other record type) instead of the optional Rule.name.
  • SureImport::Preflight: treat RejectedTransfer references as advisory — a missing
    referenced transaction is now a warning (skipped_missing_reference) rather than
    a blocking error, matching the importer's skip behaviour.
  • Family::DataImporter#import_rejected_transfers: resolve the transaction references
    with required: false so an orphaned rejected transfer is skipped (and counted as
    skipped) instead of raising under strict imports.

The exporter already filters rejected transfers to family transactions and the DB
enforces on_delete: :cascade, so new exports cannot contain these rows; this change
makes the importer resilient to existing exports that predate those guarantees.

Tests

  • test/models/sure_import_test.rb — preflight accepts a null Rule.name and reports
    an orphaned RejectedTransfer as a warning rather than an error.
  • test/models/family/data_importer_test.rb — a strict (session) import skips a
    rejected transfer whose transaction is missing while still importing the valid one.

Migration notes

None.

Notes I made while reviewing this
  • minor app/models/sure_import/preflight.rb:28 — Dropping name from Rule required fields means an empty-string ("") name — as opposed to null — now passes preflight but would raise ActiveRecord::RecordInvalid during import (Rule validates name length minimum:1 for non-nil values), aborting the whole import transaction instead of surfacing a clean preflight error. Real exports serialize null rather than empty string, so low likelihood.
  • minor app/models/sure_import/preflight.rb:274 — A RejectedTransfer that references a transaction which SHOULD exist but is genuinely missing now degrades silently to a warning + skip rather than surfacing loudly. This is the intended advisory semantics and matches the importer's skip behavior, but it does slightly reduce the diagnostic signal for a malformed export.
  • minor app/models/family/data_importer.rb:736 — In non-strict mode (import_session nil) the old code skipped orphaned rejected transfers silently; now they increment the 'skipped' summary. This is a slight behavior change to the summary counts, but it is more accurate and harmless.
  • minor app/models/import/preflight.rb:240 — sure_import_preflight_payload appends plain-string warnings (lines 241-242) into the same array that now receives {code:, message:} hash warnings from SureImport::Preflight. Previously preflight warnings were always empty, so this heterogeneous mix is newly created and is exposed in the /api/v1/imports/preflight JSON response. No crash, but inconsistent shape for any consumer.
  • minor app/models/sure_import/preflight.rb:275 — The new warning message string is hardcoded English (not t()), consistent with every other message in this preflight file. Not a new violation, but the repo's i18n rule technically covers user-facing strings and these warnings surface via the import UI/API.

Summary by CodeRabbit

  • Bug Fixes

    • Imports no longer fail when rejected transfers reference missing transactions.
    • Orphaned rejected transfers are skipped and surfaced as warnings during preflight validation.
    • Validation now supports Rule records when only an ID is provided.
    • Preflight warning messages are now returned in the expected (string) format for the API response.
  • Tests

    • Added strict-import and preflight scenarios covering skipped/reported missing references and unnamed rules.

@pro3958

pro3958 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Root cause is the importer being too strict about two legitimate export shapes: Rule.name is nullable in the model (allow_nil: true) yet preflight required it, and RejectedTransfer rows can reference transactions that were deleted after the rejection. The fix requires Rule id (which import_rules genuinely needs) instead of name, and treats dangling RejectedTransfer references as skip-with-warning rather than a hard error — RejectedTransfer is advisory match-suppression data, so skipping orphans is correct. Both added tests fail on the unfixed code and are placed in the existing import test suites. Minor: an empty-string (not null) Rule name would now pass preflight but fail at import, an unlikely edge case worth noting.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@pro3958, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f86fa3f3-5f57-44c4-89d5-14ec23070129

📥 Commits

Reviewing files that changed from the base of the PR and between ff8490a and 551c47b.

📒 Files selected for processing (6)
  • app/models/family/data_importer.rb
  • app/models/import/preflight.rb
  • app/models/sure_import/preflight.rb
  • config/locales/models/sure_import/preflight/en.yml
  • test/models/family/data_importer_test.rb
  • test/models/sure_import_test.rb
📝 Walkthrough

Walkthrough

The import preflight now accepts nullable Rule.name values and warns about missing RejectedTransfer references. The importer skips rejected transfers whose referenced transactions are missing and reports them as skipped. The schema dump also contains broad regenerated database changes.

Changes

Rejected transfer import handling

Layer / File(s) Summary
Preflight soft-reference validation
app/models/sure_import/preflight.rb, app/models/import/preflight.rb, config/locales/..., test/models/sure_import_test.rb
Rule records require id, missing RejectedTransfer references generate skipped_missing_reference warnings, and API responses serialize warnings as messages.
Importer skipping and accounting for orphaned transfers
app/models/family/data_importer.rb, test/models/family/data_importer_test.rb
Rejected transfers with unresolved inflow or outflow transactions are skipped, with created and skipped counts validated during strict import.

Database schema dump

Layer / File(s) Summary
Regenerated application schema
db/schema.rb
The schema version, extensions, tables, constraints, indexes, columns, and the transactions.transfer_id foreign key are updated across application domains.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • we-promise/sure#1755: Adds the import preflight service and API endpoint used by the updated preflight flow.

Suggested labels: contributor:verified, pr:verified

Suggested reviewers: jsonbored, jjmata

Sequence Diagram(s)

sequenceDiagram
  participant ImportAPI
  participant ImportPreflight
  participant SureImportPreflight
  participant FamilyDataImporter
  participant Database
  ImportAPI->>ImportPreflight: submit NDJSON for preflight
  ImportPreflight->>SureImportPreflight: validate Rule and RejectedTransfer references
  SureImportPreflight-->>ImportPreflight: return errors and skipped-reference warnings
  ImportAPI->>FamilyDataImporter: run strict import
  FamilyDataImporter->>Database: resolve referenced transactions optionally
  FamilyDataImporter->>Database: create valid RejectedTransfer
  FamilyDataImporter-->>ImportAPI: created and skipped summary
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning db/schema.rb contains a large unrelated schema sync that is not needed for the import bug fix. Move the schema synchronization into a separate PR or remove unrelated schema changes from this patch.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: handling nullable Rule names and orphaned rejected transfers during import.
Linked Issues check ✅ Passed The changes address #2721 by accepting Rule.id, warning on missing rejected-transfer references, and skipping orphaned records in import.
✨ 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.

@pro3958
pro3958 marked this pull request as ready for review July 23, 2026 16:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ffa380db7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/models/sure_import/preflight.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/sure_import/preflight.rb`:
- Around line 273-278: Update the warning construction in the preflight import
logic around SOFT_REFERENCE_TYPES to use an i18n translation for the complete
user-facing message, including the existing dynamic line, type, field, and value
placeholders and the skipped-during-import text; preserve the existing warning
key and error behavior.
🪄 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 Plus

Run ID: ba3f2a2f-ccf8-4056-b3ae-89148264b9ba

📥 Commits

Reviewing files that changed from the base of the PR and between 565e049 and 8ffa380.

📒 Files selected for processing (4)
  • app/models/family/data_importer.rb
  • app/models/sure_import/preflight.rb
  • test/models/family/data_importer_test.rb
  • test/models/sure_import_test.rb

Comment thread app/models/sure_import/preflight.rb Outdated
@pro3958

pro3958 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — fixed. sure_import_preflight_payload now maps each warning to its message string at the API boundary, so the response stays a homogeneous string[] matching the documented warnings schema, even when the orphaned-RejectedTransfer path emits {code, message} internally. Kept warnings as strings rather than changing the published contract (errors remain the documented ImportPreflightError objects). Added a regression test asserting the payload warnings are all strings.

@pro3958

pro3958 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Localized — the missing-reference warning and error templates now live in config/locales/models/sure_import/preflight/en.yml and interpolate line/type/field/value via I18n.t. The warning key (skipped_missing_reference) and error behavior are unchanged, and the rendered text is identical to before.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@db/schema.rb`:
- Around line 13-16: Regenerate db/schema.rb with the Rails 8.1.3 toolchain so
the schema declaration uses ActiveRecord::Schema[8.1] and the PostgreSQL
extension is represented as enable_extension "pg_catalog.plpgsql". Preserve the
existing schema contents while updating the generated Rails/pg-specific format.
🪄 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 Plus

Run ID: 0d571650-e537-401b-8df7-3c35973999ee

📥 Commits

Reviewing files that changed from the base of the PR and between 8ffa380 and ff8490a.

📒 Files selected for processing (5)
  • app/models/import/preflight.rb
  • app/models/sure_import/preflight.rb
  • config/locales/models/sure_import/preflight/en.yml
  • db/schema.rb
  • test/models/sure_import_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/sure_import/preflight.rb

Comment thread db/schema.rb
Comment on lines +13 to +16
ActiveRecord::Schema[7.2].define(version: 2026_07_14_120000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pgcrypto"
enable_extension "plpgsql"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the declared Rails/AR version the schema should target.
fd -H -t f 'Gemfile.lock' | xargs -r rg -n '^\s+(rails|activerecord) \(' 
fd -H -t f '.ruby-version|.tool-versions' | xargs -r cat
rg -n 'ActiveRecord::Schema\[' db/schema.rb
rg -n 'enable_extension' db/schema.rb

Repository: we-promise/sure

Length of output: 652


🏁 Script executed:

#!/bin/bash
set -eu

echo "== Rails/ActiveRecord lock entries =="
grep -E '^\s+(rails|activerecord) \(' Gemfile.lock || true

echo
echo "== .ruby-version / .tool-versions =="
for f in .ruby-version .tool-versions Gemfile docker-compose.yml Dockerfile; do
  [ -e "$f" ] && { echo "--- $f"; cat -n "$f"; }
done

echo
echo "== schema header and extensions =="
sed -n '1,30p' db/schema.rb
rg -n 'enable_extension' db/schema.rb

echo
echo "== migrations / schema.rb tracked status =="
git status --short db schema.rb migrations 2>/dev/null || true
git diff -- db/schema.rb 2>/dev/null | sed -n '1,120p' || true

Repository: we-promise/sure

Length of output: 8996


🌐 Web query:

ActiveRecord::Schema[7.2] Rails 8.1 db:schema:load schema format

💡 Result:

In Rails 8.1, ActiveRecord::Schema uses version-specific definitions to maintain compatibility [1][2]. A generated db/schema.rb file typically identifies the current version of the framework, such as ActiveRecord::Schema[8.1] [3][4]. This versioning mechanism allows the schema dumper to correctly use the DSL features appropriate for that specific Rails release [1][5]. When you run bin/rails db:schema:load, Rails loads the schema based on the configured format [6][7]. The format is controlled by config.active_record.schema_format, which defaults to :ruby (generating db/schema.rb) or can be set to :sql (generating db/structure.sql) [8]. Recent updates to Rails have improved the db:schema:load task to better respect the SCHEMA_FORMAT environment variable, ensuring that the loading process correctly interprets the file format when an override is provided [9][10]. When db/schema.rb is used, Rails executes the Ruby code within the ActiveRecord::Schema[version].define block to recreate the database tables and indexes [3][4][1]. If db/structure.sql is used instead, the task loads the schema by executing the raw SQL statements contained in that file [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

echo "== local Gemfile.lock excerpt around app rails pin and activerecord =="
sed -n '920,935p' Gemfile.lock
sed -n '560,580p' Gemfile.lock

echo
echo "== app configuration/schema format pins =="
rg -n 'ActiveRecord::Schema|schema_format|SCHEMA_FORMAT|db:schema:load|ActiveRecord::Base\.schema_create|schema_creation' app config lib db -g '*.rb' || true

Repository: we-promise/sure

Length of output: 1362


🌐 Web query:

"ActiveRecord::Schema[8.1]" Rails 8.1 schema.rb dump

💡 Result:

In Rails 8.1, ActiveRecord::Schema[8.1].define is the standard method used within the db/schema.rb file to define the current structure of a database [1][2]. Key aspects of this implementation include: Versioned Compatibility: The [8.1] argument specifies the Rails version compatibility layer [3]. This allows Active Record to ensure that the schema is loaded and interpreted using the behavior expected by that specific version of Rails, maintaining stability even as underlying framework internals evolve [3]. Role in Schema Dumping: When you run rails db:schema:dump, Rails introspects your database and generates the db/schema.rb file, which acts as a database-agnostic, Ruby-based representation of your schema [1][4]. The call to ActiveRecord::Schema[8.1].define wraps this generated schema information [1][3]. New Features in Rails 8.1: A notable change introduced in Rails 8.1 is that table columns are now sorted alphabetically within the schema dump [4]. This behavior provides a consistent, deterministic order for columns, which helps reduce unnecessary diffs in version control when multiple developers modify the same tables [5][4]. When upgrading to Rails 8.1, running a schema dump will trigger this reordering, which may appear as a significant change in your initial commit [4].

Citations:


Keep db/schema.rb in the Rails 8.1 schema format.

The project is pinned to Rails 8.1.3, but this file declares ActiveRecord::Schema[7.2] and uses enable_extension "plpgsql" instead of the Rails 8.1/pg 1.5 enable_extension "pg_catalog.plpgsql" form. Regenerate db/schema.rb under the 8.1 toolchain; db:schema:load on 8.1 may execute the mismatched DSL and future dumps can churn.

🤖 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 `@db/schema.rb` around lines 13 - 16, Regenerate db/schema.rb with the Rails
8.1.3 toolchain so the schema declaration uses ActiveRecord::Schema[8.1] and the
PostgreSQL extension is represented as enable_extension "pg_catalog.plpgsql".
Preserve the existing schema contents while updating the generated
Rails/pg-specific format.

agentloop and others added 3 commits July 23, 2026 14:55
Importing a full all.ndjson export aborted on data that is actually
valid. The preflight listed name as a required field for Rule, but
rules.name is nullable and the model allows it, so a single rule with
"name": null blocked the entire import. Separately, a RejectedTransfer
whose referenced transaction had been deleted raised a hard
missing_reference error in preflight and a MissingReferenceError in
strict mode, even though the importer already had a skip path for it.

Require Rule.id instead of Rule.name in preflight, matching the field
the importer actually needs. Treat RejectedTransfer references as
advisory: a missing referenced transaction becomes a warning, and the
importer resolves the references with required: false so the orphaned
row is skipped and counted instead of raising.

Fixes we-promise#2721
… boundary

The orphaned-RejectedTransfer path emits warnings as {code, message} hashes via
add_warning, but the published OpenAPI contract documents /api/v1/imports/preflight
warnings as strings. sure_import_preflight_payload copied them through unchanged, so
the endpoint returned a heterogeneous array once that path was exercised and
contract-generated clients could fail to deserialize.

Map warnings to their human-readable message at the API boundary so the array stays
homogeneous strings, matching the documented schema. The internal {code, message}
shape is unchanged. Adds a regression test.
The missing-reference warning and error are user-facing (returned in
Result#payload[:warnings]/[:errors]) but were hard-coded. Move the full templates
to config/locales/models/sure_import/preflight/en.yml and interpolate line, type,
field, and value, per the i18n coding guideline. Warning key and error behavior are
unchanged, and the rendered text is identical.

jjmata commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Review

What it does: Two related preflight/import fixes for the full NDJSON (SureImport) flow:

  1. SureImport::Preflight required Rule.name, but rules.name is nullable in the schema and the model (allow_nil: true). The PR switches the required field to Rule.id, which is what import_rules actually needs.
  2. RejectedTransfer references to a missing transaction previously raised a hard missing_reference preflight error, and Family::DataImporter#import_rejected_transfers raised MissingReferenceError under strict (session) imports instead of skipping. Now these are treated as advisory: preflight emits a skipped_missing_reference warning, and the importer resolves refs with required: false, skipping and counting the row.

Correctness: Verified by tracing the code paths — Rule.assign_attributes(name: data["name"]) handles nil fine, mapped_id(required: false) returns nil without raising even in strict mode, and import! runs inside a single Import.transaction, so the old strict-mode raise really did abort the whole import. The added SureImport::Preflight change also fixes a real (if previously latent) contract bug: warnings are documented as string[] in the OpenAPI schema, but SureImport::Preflight started returning {code, message} hashes — Import::Preflight#sure_import_preflight_payload now normalizes them to strings, matching the schema. i18n and locale key usage look correct.

Tests: Both new tests trace back to a real failure on unfixed code (confirmed by manual trace, not by running Rails locally — Ruby version mismatch in this sandbox prevented execution): the preflight test fails pre-fix because name: nil trips the required-field check and the orphaned RejectedTransfer trips missing_reference; the strict-import test fails pre-fix because mapped_id raises MissingReferenceError inside the transaction.

Minor (already disclosed by author): an empty-string Rule.name now bypasses preflight (only id is checked) and would raise ActiveRecord::RecordInvalid at import time, aborting the whole strict-import transaction instead of a clean preflight error. Real exports serialize null, not "", so low real-world impact — not blocking.

No other call sites assume non-null Rule.name or hard-require RejectedTransfer references. CI is green.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Error importing with all.ndjson

2 participants