Skip to content

feat(imports): verify Sure NDJSON import readback#1869

Merged
jjmata merged 4 commits into
we-promise:mainfrom
JSONbored:codex/import-authoritative-history
May 20, 2026
Merged

feat(imports): verify Sure NDJSON import readback#1869
jjmata merged 4 commits into
we-promise:mainfrom
JSONbored:codex/import-authoritative-history

Conversation

@JSONbored
Copy link
Copy Markdown
Contributor

@JSONbored JSONbored commented May 19, 2026

Summary

Adds durable Sure NDJSON import readback verification so completed imports carry expected upload counts plus family-scoped post-import database deltas.

What changed

  • Persist expected Sure NDJSON record counts when an import file is uploaded.
  • Record before/after family-scoped readback counts after publish and classify verification as matched, mismatch, failed, or reverted.
  • Expose verification on GET /api/v1/imports/:id and the completed Sure import UI.
  • Count every importer-supported Sure NDJSON record type in the dry-run/preflight count map.
  • Add invariant coverage for tenant isolation, skipped-row count truth, failed-import rollback, API response truth, and revert compatibility.
  • Update rswag docs-only schemas and regenerate docs/api/openapi.yaml.

Why

A completed import status is not enough proof for large histories. This gives operators and API consumers a durable count-level check: what the uploaded package claimed, what the database looked like before and after import, and whether the readback delta matches expected records.

Related context

No existing open issue is fully closed by this slice.

Validation

  • bin/rails test test/models/sure_import_test.rb — 24 tests, 78 assertions.
  • bin/rails test test/controllers/api/v1/imports_controller_test.rb — 55 tests, 325 assertions.
  • RAILS_ENV=test bundle exec rake rswag:specs:swaggerize — 283 examples, 0 failures.
  • bin/rubocop app/models/sure_import.rb app/helpers/imports_helper.rb test/models/sure_import_test.rb test/controllers/api/v1/imports_controller_test.rb
  • bundle exec erb_lint app/views/imports/_success.html.erb
  • npm run lint
  • bin/rails zeitwerk:check
  • bin/brakeman --no-pager
  • bin/importmap audit
  • git diff --check

Rails commands were run with local test database credentials configured.

Notes

This closes the count-level proof gap for single-file Sure NDJSON imports. It does not add row-level fingerprints, split package resume behavior, or historical balance parity checks.

Future work, not included here:

  • Row-level import verification with deterministic source fingerprints.
  • Historical balance/valuation parity by account/date/currency.
  • Closed-account Transactions-page readback after closed-account API behavior lands.
  • CSV/API category/tag auto-mapping, related to Bug: No automatic category mapping when importing via API #1867.
  • Operator UI for chunk upload/resume.
  • Import verification report download.
  • Retention/cleanup policy for old chunks, mappings, and attachments.
  • Worker health warning for stale async import/reset status.
  • Attachment portability.
  • Recurring transaction/transfer schedule portability.
  • Rule portability without surprise mutation.
  • Streaming NDJSON parsing for very large packages.
  • Manifest-level package integrity checks.

Summary by CodeRabbit

  • New Features
    • Import readback verification: track expected vs. actual record counts; success page shows status, checked counts, and mismatches; API responses include verification details.
  • Localization
    • Added locale labels for dry-run import resource types and verification UI text.
  • Database
    • Added persistent fields to store expected counts and readback verification results.
  • Docs
    • OpenAPI updated to document verification payloads.
  • Tests
    • Added coverage for verification flows, views, and API output.
  • Style
    • Updated success icon background styling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8ab9b8ac-79dd-47ee-9879-261ad1ab473b

📥 Commits

Reviewing files that changed from the base of the PR and between a295282 and 73fa3f0.

📒 Files selected for processing (4)
  • app/helpers/imports_helper.rb
  • app/views/imports/_success.html.erb
  • test/helpers/imports_helper_test.rb
  • test/models/sure_import_test.rb

📝 Walkthrough

Walkthrough

Persist expected NDJSON counts on imports, snapshot pre/post readback counts during SureImport, compute deltas and mismatches, record verification results (matched/mismatch/failed/reverted), expose verification in the API/OpenAPI and UI, and add tests covering flows.

Changes

SureImport Readback Verification

Layer / File(s) Summary
Database schema for verification tracking
db/migrate/20260519100000_add_sure_import_verification_to_imports.rb, db/schema.rb
Migration creates two JSONB columns (expected_record_counts and readback_verification) on the imports table; schema file reflects the version bump and new columns with default empty objects.
API response & OpenAPI/Swagger schemas
app/views/api/v1/imports/show.json.jbuilder, docs/api/openapi.yaml, spec/swagger_helper.rb, spec/requests/api/v1/imports_spec.rb
Jbuilder template conditionally includes verification field; OpenAPI and Swagger add ImportVerificationReadback and ImportVerification schemas and extend ImportDetail; endpoint docs updated.
Helpers, UI, and i18n for verification display
app/helpers/imports_helper.rb, app/views/imports/_success.html.erb, config/locales/views/imports/en.yml
dry_run_resource uses I18n and adds new resource keys; ImportVerificationView struct added to shape verification payload for UI; success partial renders verification panel and mismatches preview; locale strings added.
Importable type mappings and expected-count computation
app/models/sure_import.rb
IMPORTABLE_NDJSON_TYPES extended (including Balance); VERIFICATION_STATUSES added; class methods compute expected_record_counts from NDJSON content or line-type counts and normalize keys.
Import flow integration and verification core
app/models/sure_import.rb
import! now syncs NDJSON-derived counts, snapshots pre-import readback, runs Family::DataImporter, and persists readback verification (matched/mismatch/failed) with timestamps and errors; public accessors and reset behavior added; revert updated to reset verification when pending.
Verification behavior and integration tests
test/models/sure_import_test.rb, test/controllers/api/v1/imports_controller_test.rb, test/helpers/imports_helper_test.rb
Model tests validate expected counts, publish outcomes (matched/mismatch/failed), revert behavior, and logging on verification write failures; controller/integration test asserts API returns verification payload; helper tests cover dry-run labels and ImportVerificationView behavior; NDJSON test helpers added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

pr:verified

Suggested reviewers

  • jjmata

Poem

🐰 I hopped through JSON lines, counting each small part,
Took before-and-after snapshots straight from the heart,
When mismatches whispered, I nudged them into view,
Matched rows nodded, failures logged true,
Now imports burrow safe — hop on, code, restart!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.50% 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 title accurately summarizes the main feature: adding verification logic for Sure NDJSON import readback, which is the core change across the codebase.
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.

@JSONbored JSONbored marked this pull request as ready for review May 20, 2026 00:16
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: 2

🤖 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/helpers/imports_helper.rb`:
- Line 35: Replace the hard-coded English labels in the DryRunResource.new calls
(e.g., the label: "Balances" instance in imports_helper.rb and the similar
entries at the other referenced lines) with i18n lookups using t() keys; update
each DryRunResource.new(label: ...) to call t with a descriptive key (for
example imports.dry_run.balances, imports.dry_run.<other_resource>) and add
those keys to the locale files so the labels render in other languages. Ensure
you update all occurrences mentioned (lines around the Balances line and the
other listed entries) and keep the icon, text_class, and bg_class unchanged.

In `@app/views/imports/_success.html.erb`:
- Around line 15-18: Summary: Guard against nil verification payload before
calling fetch on it. Fix: change how verification is obtained from
import.verification_payload[:readback] so it never yields nil (e.g., fallback to
an empty Hash or use safe navigation), then keep the existing fetch calls for
verification_status, checked_counts, and mismatches; update the reference to
import.verification_payload, verification, and the variables
verification_status, checked_counts, mismatches accordingly so NoMethodError
cannot occur when readback is nil.
🪄 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: 64b02e13-818e-4b36-a279-33518e844c30

📥 Commits

Reviewing files that changed from the base of the PR and between e8ce286 and 4a34c2e.

📒 Files selected for processing (12)
  • app/helpers/imports_helper.rb
  • app/models/sure_import.rb
  • app/views/api/v1/imports/show.json.jbuilder
  • app/views/imports/_success.html.erb
  • config/locales/views/imports/en.yml
  • db/migrate/20260519100000_add_sure_import_verification_to_imports.rb
  • db/schema.rb
  • docs/api/openapi.yaml
  • spec/requests/api/v1/imports_spec.rb
  • spec/swagger_helper.rb
  • test/controllers/api/v1/imports_controller_test.rb
  • test/models/sure_import_test.rb

Comment thread app/helpers/imports_helper.rb Outdated
Comment thread app/views/imports/_success.html.erb Outdated
Copy link
Copy Markdown

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

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: 4a34c2e2f9

ℹ️ 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.rb Outdated
Copy link
Copy Markdown
Collaborator

jjmata commented May 20, 2026

Solid feature with comprehensive test coverage. A few observations:

readback_count_snapshot query volume — this method issues 12+ separate DB queries and is called twice per import (before and after). For families with many records this is fast, but worth being aware of if imports become a hot path.

sync_ndjson_counts! fallback in import! — the if expected_record_counts.blank? guard is a good safety net, but it means an import that was never uploaded properly could proceed without accurate expected counts and end up as matched trivially (all expected counts are 0). Consider whether that state needs a distinct verification_status or at minimum a logged warning.

revert flowreset_readback_verification! is called after super, and it checks if pending?. Since super transitions the import to pending, the check passes and the verification is correctly marked reverted. Looks good, just worth having an explicit test that the not pending? branch (e.g. if super fails) doesn't leave stale verification data.

record_failed_readback_verification! rescue — the inner rescue logs and swallows any error from building the verification record. This is the right call (don't mask the original import error), but it means a verification failure during failure handling could be silently dropped. The Rails.logger.warn is appropriate; just confirm it goes to a monitored log level in production.


Generated by Claude Code

@superagent-security superagent-security Bot added the contributor:verified Contributor passed trust analysis. label May 20, 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.

🧹 Nitpick comments (4)
app/helpers/imports_helper.rb (1)

34-48: ⚡ Quick win

Use design-system functional tokens for the updated resource classes.

Lines 34–48 still use raw palette classes (text-*-500, bg-*-500/5). Please switch these entries to functional tokens from app/assets/tailwind/sure-design-system.css for consistency and theme safety.

As per coding guidelines: “Always prefer using functional tokens defined in sure-design-system.css … rather than raw Tailwind utilities.”

🤖 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/helpers/imports_helper.rb` around lines 34 - 48, The DryRunResource
entries (e.g., transactions, balances, accounts, categories, tags, rules,
merchants, recurring_transactions, transfers, rejected_transfers, trades,
holdings, valuations, budgets, budget_categories) use raw palette classes like
"text-*-500" and "bg-*-500/5"; replace those text_class and bg_class values with
the corresponding functional token classes defined in sure-design-system.css
(e.g., use semantic text tokens and background container/selected tokens rather
than literal color utilities) so all DryRunResource.new(...) calls reference
design-system tokens consistently; update each DryRunResource instantiation to
map the existing color intent to the proper token names from
app/assets/tailwind/sure-design-system.css.
app/views/imports/_success.html.erb (1)

26-30: ⚡ Quick win

Format displayed counts server-side for readability.

At Line 26, Line 30, and Line 39, numeric output is unformatted. Use server-side number formatting (e.g., number_with_delimiter) for large values.

Proposed patch
-            <p class="font-medium text-primary"><%= verification.checked_total %></p>
+            <p class="font-medium text-primary"><%= number_with_delimiter(verification.checked_total) %></p>
@@
-            <p class="font-medium text-primary"><%= verification.mismatches_count %></p>
+            <p class="font-medium text-primary"><%= number_with_delimiter(verification.mismatches_count) %></p>
@@
-                <span class="font-medium text-primary"><%= counts["actual"] %> / <%= counts["expected"] %></span>
+                <span class="font-medium text-primary">
+                  <%= number_with_delimiter(counts["actual"].to_i) %> /
+                  <%= number_with_delimiter(counts["expected"].to_i) %>
+                </span>

As per coding guidelines: “Format currencies, numbers, dates, and other values server-side.”

Also applies to: 39-39

🤖 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/views/imports/_success.html.erb` around lines 26 - 30, The numeric counts
rendered in the template (verification.checked_total and
verification.mismatches_count and the third numeric output at the other
occurrence) must be formatted server-side for readability: wrap each raw numeric
interpolation with the Rails helper number_with_delimiter (e.g., replace <%=
verification.checked_total %> with
number_with_delimiter(verification.checked_total) and do the same for
verification.mismatches_count and the other count), ensuring the view calls
number_with_delimiter(...) for all three places so large values show delimiters.
test/models/sure_import_test.rb (1)

357-361: ⚡ Quick win

Prefer mocha-style stubbing/expectations for logger-path test consistency.

Use mocha (stubs / expects) here instead of Minitest stub blocks to align with repo test conventions.

Proposed refactor
-    logged_messages = []
-
-    Rails.logger.stub(:warn, ->(message) { logged_messages << message }) do
-      `@import.stub`(:update_columns, ->(*) { raise StandardError, "verification write failed" }) do
-        `@import.send`(:record_failed_readback_verification!, before_counts:, error: original_error)
-      end
-    end
-
-    assert_match(/Failed to record Sure import readback verification/, logged_messages.first)
-    assert_match(/verification write failed/, logged_messages.first)
+    `@import.stubs`(:update_columns).raises(StandardError.new("verification write failed"))
+    Rails.logger.expects(:warn).with(regexp_matches(/Failed to record Sure import readback verification.*verification write failed/))
+
+    `@import.send`(:record_failed_readback_verification!, before_counts:, error: original_error)

As per coding guidelines, "Use the mocha gem for stubs and mocks".

🤖 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 `@test/models/sure_import_test.rb` around lines 357 - 361, Replace Minitest
block stubs with mocha-style stubs: stub the Rails.logger.warn and the
`@import.update_columns` using mocha's stubs/raises so the test follows repo
conventions. Specifically, change the Rails.logger.stub(:warn, ...) block to use
Rails.logger.stubs(:warn) and capture the warned message into logged_messages
via the stub's block, and change `@import.stub`(:update_columns, ->(*) { raise
StandardError, "verification write failed" }) to
`@import.stubs`(:update_columns).raises(StandardError, "verification write
failed"); keep the call to `@import.send`(:record_failed_readback_verification!,
before_counts:, error: original_error) unchanged. Ensure mocha is available in
the test environment.
test/helpers/imports_helper_test.rb (1)

20-22: ⚡ Quick win

Use repo-standard test doubles (mocha/OpenStruct) instead of Minitest::Mock.

These tests can use OpenStruct (or minimal mocha stubs) to match repo conventions and avoid explicit verify calls.

Proposed refactor
-    import = Minitest::Mock.new
-    import.expect(:verification_payload, {})
+    import = OpenStruct.new(verification_payload: {})
@@
-    import.verify
@@
-    import = Minitest::Mock.new
-    import.expect(:verification_payload, { readback: nil })
+    import = OpenStruct.new(verification_payload: { readback: nil })
@@
-    import.verify
@@
-    import = Minitest::Mock.new
-    import.expect(
-      :verification_payload,
-      {
-        readback: {
-          status: "mismatch",
-          checked_counts: { accounts: 1, transactions: "2" },
-          mismatches: {
-            accounts: { expected: 1, actual: 0 },
-            transactions: { expected: 2, actual: 1 },
-            categories: { expected: 3, actual: 2 },
-            tags: { expected: 4, actual: 3 }
-          }
-        }
-      }
-    )
+    import = OpenStruct.new(
+      verification_payload: {
+        readback: {
+          status: "mismatch",
+          checked_counts: { accounts: 1, transactions: "2" },
+          mismatches: {
+            accounts: { expected: 1, actual: 0 },
+            transactions: { expected: 2, actual: 1 },
+            categories: { expected: 3, actual: 2 },
+            tags: { expected: 4, actual: 3 }
+          }
+        }
+      }
+    )
@@
-    import.verify

As per coding guidelines, "Use the mocha gem for stubs and mocks" and "Always prefer OpenStruct when creating mock instances".

Also applies to: 34-36, 48-51

🤖 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 `@test/helpers/imports_helper_test.rb` around lines 20 - 22, Replace the
Minitest::Mock usage with the repo-standard test doubles: create an OpenStruct
(or a mocha stub) that responds to verification_payload instead of calling
Minitest::Mock and expect; specifically, change the `import =
Minitest::Mock.new` / `import.expect(:verification_payload, {})` pattern to
something like an OpenStruct initialized with verification_payload (or a mocha
stub that stubs `verification_payload`) and apply the same replacement for the
other mock spots noted (the other `import` mock instances).
🤖 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/helpers/imports_helper.rb`:
- Around line 34-48: The DryRunResource entries (e.g., transactions, balances,
accounts, categories, tags, rules, merchants, recurring_transactions, transfers,
rejected_transfers, trades, holdings, valuations, budgets, budget_categories)
use raw palette classes like "text-*-500" and "bg-*-500/5"; replace those
text_class and bg_class values with the corresponding functional token classes
defined in sure-design-system.css (e.g., use semantic text tokens and background
container/selected tokens rather than literal color utilities) so all
DryRunResource.new(...) calls reference design-system tokens consistently;
update each DryRunResource instantiation to map the existing color intent to the
proper token names from app/assets/tailwind/sure-design-system.css.

In `@app/views/imports/_success.html.erb`:
- Around line 26-30: The numeric counts rendered in the template
(verification.checked_total and verification.mismatches_count and the third
numeric output at the other occurrence) must be formatted server-side for
readability: wrap each raw numeric interpolation with the Rails helper
number_with_delimiter (e.g., replace <%= verification.checked_total %> with
number_with_delimiter(verification.checked_total) and do the same for
verification.mismatches_count and the other count), ensuring the view calls
number_with_delimiter(...) for all three places so large values show delimiters.

In `@test/helpers/imports_helper_test.rb`:
- Around line 20-22: Replace the Minitest::Mock usage with the repo-standard
test doubles: create an OpenStruct (or a mocha stub) that responds to
verification_payload instead of calling Minitest::Mock and expect; specifically,
change the `import = Minitest::Mock.new` / `import.expect(:verification_payload,
{})` pattern to something like an OpenStruct initialized with
verification_payload (or a mocha stub that stubs `verification_payload`) and
apply the same replacement for the other mock spots noted (the other `import`
mock instances).

In `@test/models/sure_import_test.rb`:
- Around line 357-361: Replace Minitest block stubs with mocha-style stubs: stub
the Rails.logger.warn and the `@import.update_columns` using mocha's stubs/raises
so the test follows repo conventions. Specifically, change the
Rails.logger.stub(:warn, ...) block to use Rails.logger.stubs(:warn) and capture
the warned message into logged_messages via the stub's block, and change
`@import.stub`(:update_columns, ->(*) { raise StandardError, "verification write
failed" }) to `@import.stubs`(:update_columns).raises(StandardError, "verification
write failed"); keep the call to
`@import.send`(:record_failed_readback_verification!, before_counts:, error:
original_error) unchanged. Ensure mocha is available in the test environment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d05bb50a-a08e-49fb-8155-5f9f0e0b5707

📥 Commits

Reviewing files that changed from the base of the PR and between 4a34c2e and a295282.

📒 Files selected for processing (7)
  • app/helpers/imports_helper.rb
  • app/models/sure_import.rb
  • app/views/imports/_success.html.erb
  • config/locales/views/imports/en.yml
  • test/controllers/api/v1/imports_controller_test.rb
  • test/helpers/imports_helper_test.rb
  • test/models/sure_import_test.rb

@JSONbored
Copy link
Copy Markdown
Contributor Author

Solid feature with comprehensive test coverage. A few observations:

readback_count_snapshot query volume — this method issues 12+ separate DB queries and is called twice per import (before and after). For families with many records this is fast, but worth being aware of if imports become a hot path.

sync_ndjson_counts! fallback in import! — the if expected_record_counts.blank? guard is a good safety net, but it means an import that was never uploaded properly could proceed without accurate expected counts and end up as matched trivially (all expected counts are 0). Consider whether that state needs a distinct verification_status or at minimum a logged warning.

revert flowreset_readback_verification! is called after super, and it checks if pending?. Since super transitions the import to pending, the check passes and the verification is correctly marked reverted. Looks good, just worth having an explicit test that the not pending? branch (e.g. if super fails) doesn't leave stale verification data.

record_failed_readback_verification! rescue — the inner rescue logs and swallows any error from building the verification record. This is the right call (don't mask the original import error), but it means a verification failure during failure handling could be silently dropped. The Rails.logger.warn is appropriate; just confirm it goes to a monitored log level in production.

Generated by Claude Code

Done, done, done! Let me know if anything else is needed - will get it done ASAP!

@jjmata jjmata added this to the v0.7.1 milestone May 20, 2026
@jjmata jjmata merged commit 6558953 into we-promise:main May 20, 2026
11 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.

Development

Successfully merging this pull request may close these issues.

2 participants