Files
sure/app/views/redbark_items/setup_accounts.html.erb
Oscar c6a240a183 feat(redbark): add australian bank sync (redbark) (#2794)
* add redbark provider integration

- per family api key provider, built like the lunchflow integration
- syncs accounts, balances and transactions from api.redbark.com
- account setup flow, settings panel, locales and routes
- tests and fixtures

* harden redbark integration based on prior provider pr feedback

- use DebugLogEntry.capture for sync/import/unlink failures
- retry 429s and 5xxs with backoff, raise on page cap instead of truncating
- keep raw response bodies out of logs and errors
- not null constraints on account columns, migration base 7.2
- persist ignored flag for skipped accounts so they stop nagging setup
- validate api key on every save, re-arm status on key rotation
- destroy aborts if unlink fails, atomic account create and link
- require_admin on mutating actions, see_other on error redirects
- single grouped query for item account counts
- i18n default connection name, blank password field value
- controller and provider tests

* fix issues found in second review sweep

- add missing syncable scope, without it every family sync raises
- kick off a sync on connection create and on key rotation
- setup dialog fetches accounts inline for fresh connections and shows api errors
- skip balance write when no balance has been fetched yet, never anchor a false zero
- exclude stale and non banking accounts from the batched balances call, per account fallback if the batch is rejected
- detect the server row ceiling and empty pages instead of silently truncating history
- user sync start date only governs the initial backfill, incremental after that
- fetch connections before the per account loop so auth errors propagate once
- drop untemplated index/show/new/edit routes and dead preload/link_accounts actions
- stable dom id on the settings panel so repeat turbo replaces keep working

* skip brokerage connections, found in live testing

- the transactions endpoint 400s for brokerage connections, they belong to /v1/trades
- only import accounts from banking and documents connections
- guard transaction fetches for any legacy linked non banking account

* address review feedback

- treat the truncation header as a pagination signal: split the date window and refetch instead of failing the account
- prune stale pending rows from the snapshot so settled pendings cant come back as duplicates
- block linking a sure account that already has another provider feed
- count setup failures separately from skips and surface an error instead of "all skipped"
- add not nulls on redbark_items name and api key
- enqueue the destroy job after the flag commits, not inside the transaction
- swap bg-gray-400 for bg-surface-inset, drop amounts from info logs, remove i18n default fallbacks
- tests for window splitting, pending pruning and encrypted payload round trip

* fix issues from convention review

- benign skips (unlinked account, blank id, unparseable rows) no longer count as failures, tracked separately so a clean batch reports success
- currency parsing goes through extract_currency so hash shaped payloads resolve instead of falling to the default
- merchant ids use truncated sha256 instead of md5
- debug log entries for import failures and account sync scheduling failures

* bound the raw transactions snapshot to the fetch window

- trim raw_transactions_payload to the current fetch window on merge, same as brex
- keep rows without a parseable date, drop settled pendings as before
- surface skipped rows in the aggregate debug log entry with imported/skipped counts
2026-07-26 07:40:25 +02:00

76 lines
3.3 KiB
Plaintext

<% content_for :title, t(".title") %>
<%= render DS::Dialog.new(disable_click_outside: true) do |dialog| %>
<% dialog.with_header(title: t(".title")) do %>
<div class="flex items-center gap-2">
<%= icon "landmark", class: "text-primary" %>
<span class="text-primary"><%= t(".subtitle") %></span>
</div>
<% end %>
<% dialog.with_body do %>
<div class="space-y-6">
<% if @api_error.present? %>
<%= render DS::Alert.new(
title: t(".fetch_failed"),
message: @api_error,
variant: :error
) %>
<% end %>
<% if @unlinked_accounts.blank? %>
<div class="flex flex-col items-center justify-center py-6 space-y-3">
<% if @api_error.blank? %>
<%= icon "check-circle", size: "lg", class: "text-success" %>
<p class="text-primary text-center font-medium"><%= t(".all_accounts_linked") %></p>
<% end %>
<%= render DS::Link.new(text: t(".done"), variant: "primary", href: accounts_path, frame: "_top") %>
</div>
<% else %>
<%= form_with url: complete_account_setup_redbark_item_path(@redbark_item), method: :post, data: { turbo_frame: "_top" }, class: "space-y-6" do %>
<div class="bg-surface border border-primary p-4 rounded-lg">
<div class="flex items-start gap-3">
<%= icon "info", size: "sm", class: "text-primary mt-0.5 flex-shrink-0" %>
<p class="text-sm text-primary"><%= t(".choose_account_type") %></p>
</div>
</div>
<div class="space-y-4 max-h-96 overflow-y-auto">
<% @unlinked_accounts.each do |redbark_account| %>
<div class="border border-primary rounded-lg p-4">
<h4 class="font-medium text-primary"><%= redbark_account.name %></h4>
<p class="text-xs text-secondary mb-3">
<%= [ redbark_account.account_type&.titleize, redbark_account.currency ].compact.join(" · ") %>
</p>
<label class="block text-sm text-secondary mb-1" for="account_type_<%= redbark_account.id %>">
<%= t(".account_type_label") %>
</label>
<select id="account_type_<%= redbark_account.id %>"
name="accounts[<%= redbark_account.id %>][account_type]"
class="bg-container border border-primary rounded px-2 py-1 text-sm text-primary w-full">
<option value="depository" selected><%= t(".account_types.depository") %></option>
<option value="credit_card"><%= t(".account_types.credit_card") %></option>
<option value="loan"><%= t(".account_types.loan") %></option>
<option value="skip"><%= t(".account_types.skip") %></option>
</select>
</div>
<% end %>
</div>
<div class="flex gap-3">
<%= render DS::Button.new(
text: t(".create_accounts"),
variant: "primary",
icon: "plus",
type: "submit",
class: "flex-1"
) %>
<%= render DS::Link.new(text: t(".cancel"), variant: "secondary", href: accounts_path, frame: "_top") %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% end %>