mirror of
https://github.com/we-promise/sure.git
synced 2026-05-25 05:24:57 +00:00
* feat(sync): add Brex provider schema Adds Brex item and account tables with per-family credentials, scoped upstream account uniqueness, encrypted token storage, and sanitized provider payload columns. * feat(sync): add Brex provider core Adds Brex item/account models, provider client and adapter support, family connection helpers, and provider enum registration for read-only Brex cash and card data. * feat(sync): add Brex import pipeline Adds Brex account discovery, linked-account sync, cash/card balance processors, transaction import, sanitized metadata handling, and idempotent provider entry processing. * feat(sync): add Brex connection flows Adds Mercury-style Brex connection management, explicit item-scoped account selection and linking, settings provider UI, account index visibility, localized copy, and per-item cache handling. * test(sync): cover Brex provider workflows Adds targeted coverage for Brex provider requests, adapter config, item/account guards, importer behavior, entry processing, and Mercury-style controller flows. * fix(sync): align Brex API edge cases Tightens Brex account fetching against the official card-account response shape, sends transaction start filters as RFC3339 date-times, and keeps provider error bodies out of user-facing messages while expanding provider client guard coverage. * fix(sync): harden Brex provider integration Restrict Brex API base URLs to official hosts, tighten account-selection UI behavior, and add tests for invalid credentials, cache scoping, and provider setup edge cases. * test(sync): avoid Brex secret-shaped fixtures * refactor(sync): extract Brex account flows * fix(sync): address Brex provider review feedback * fix(sync): address Brex review follow-ups Move remaining Brex review cleanup into focused model behavior, tighten link/setup edge cases, localize summaries, and add regression coverage from CodeRabbit feedback. Also records the security-review pass as no-findings after diff-scoped inspection and Brakeman validation. * refactor(sync): split Brex account flow controllers Route Brex account selection and setup actions through small namespaced controllers while keeping existing URLs and helpers stable. Business flow remains in BrexItem::AccountFlow; the main Brex item controller now only handles connection CRUD, provider-panel rendering, destroy, and sync. * fix(sync): address Brex CodeRabbit review * fix(sync): address Brex follow-up review * fix(sync): address Brex review follow-ups * fix(sync): address Brex sync review findings * fix(sync): polish Brex review copy and errors * fix(sync): register Brex provider health * fix(sync): polish Brex bank sync presentation * fix(sync): address Brex review follow-ups * fix(sync): tighten Brex setup params * test(api): stabilize usage rate-limit window * fix(sync): polish Brex setup flow nits * fix(sync): harden Brex setup params * fix(sync): finalize Brex review cleanup --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
181 lines
5.7 KiB
Ruby
181 lines
5.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "digest/md5"
|
|
|
|
class BrexEntry::Processor
|
|
include CurrencyNormalizable
|
|
|
|
def initialize(brex_transaction, brex_account:)
|
|
@brex_transaction = brex_transaction
|
|
@brex_account = brex_account
|
|
end
|
|
|
|
def process
|
|
cached_external_id = nil
|
|
cached_external_id = external_id
|
|
|
|
unless account.present?
|
|
Rails.logger.warn "BrexEntry::Processor - No linked account for brex_account #{brex_account.id}, skipping transaction #{cached_external_id}"
|
|
return :skipped
|
|
end
|
|
|
|
import_adapter.import_transaction(
|
|
external_id: cached_external_id,
|
|
amount: amount,
|
|
currency: currency,
|
|
date: date,
|
|
name: name,
|
|
source: "brex",
|
|
merchant: merchant,
|
|
notes: notes,
|
|
extra: extra
|
|
)
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "BrexEntry::Processor - Validation error for transaction #{cached_external_id || safe_external_id}: #{e.message}"
|
|
raise
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.error "BrexEntry::Processor - Failed to save transaction #{cached_external_id || safe_external_id}: #{e.message}"
|
|
raise StandardError.new("Failed to import transaction: #{e.message}")
|
|
rescue => e
|
|
Rails.logger.error "BrexEntry::Processor - Unexpected error processing transaction #{cached_external_id || safe_external_id}: #{e.class} - #{e.message}"
|
|
Rails.logger.error Array(e.backtrace).join("\n")
|
|
raise StandardError.new("Unexpected error importing transaction: #{e.message}")
|
|
end
|
|
|
|
private
|
|
attr_reader :brex_transaction, :brex_account
|
|
|
|
def import_adapter
|
|
@import_adapter ||= Account::ProviderImportAdapter.new(account)
|
|
end
|
|
|
|
def account
|
|
@account ||= brex_account.current_account
|
|
end
|
|
|
|
def data
|
|
@data ||= brex_transaction.with_indifferent_access
|
|
end
|
|
|
|
def external_id
|
|
id = data[:id].presence
|
|
raise ArgumentError, "Brex transaction missing required field 'id'" unless id
|
|
|
|
"brex_#{id}"
|
|
end
|
|
|
|
def safe_external_id
|
|
external_id
|
|
rescue ArgumentError
|
|
"brex_unknown"
|
|
end
|
|
|
|
def name
|
|
data[:description].presence ||
|
|
merchant_payload[:raw_descriptor].presence ||
|
|
merchant_payload[:name].presence ||
|
|
I18n.t("brex_items.entries.default_name")
|
|
end
|
|
|
|
def notes
|
|
note_parts = []
|
|
note_parts << data[:type] if data[:type].present?
|
|
note_parts << data[:expense_id] if data[:expense_id].present?
|
|
note_parts.any? ? note_parts.join(" - ") : nil
|
|
end
|
|
|
|
def merchant
|
|
merchant_name = merchant_payload[:raw_descriptor].presence || merchant_payload[:name].presence
|
|
return @merchant if instance_variable_defined?(:@merchant)
|
|
return @merchant = nil if merchant_name.blank?
|
|
|
|
merchant_name = merchant_name.to_s.strip
|
|
return @merchant = nil if merchant_name.blank?
|
|
|
|
merchant_id = Digest::MD5.hexdigest(merchant_name.downcase)
|
|
|
|
@merchant = import_adapter.find_or_create_merchant(
|
|
provider_merchant_id: "brex_merchant_#{merchant_id}",
|
|
name: merchant_name,
|
|
source: "brex"
|
|
)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
Rails.logger.error "BrexEntry::Processor - Failed to create merchant '#{merchant_name}': #{e.message}"
|
|
@merchant = nil
|
|
end
|
|
|
|
def merchant_payload
|
|
@merchant_payload ||= begin
|
|
payload = data[:merchant]
|
|
payload.is_a?(Hash) ? payload.with_indifferent_access : {}
|
|
end
|
|
end
|
|
|
|
def amount
|
|
BrexAccount.money_to_decimal(data[:amount]) || BigDecimal("0")
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "Failed to parse Brex transaction amount: #{data[:amount].inspect} - #{e.message}"
|
|
raise
|
|
end
|
|
|
|
def currency
|
|
amount_currency = transaction_amount_currency
|
|
log_invalid_currency(amount_currency) if amount_currency.blank? && data[:amount].present?
|
|
|
|
parse_currency(amount_currency) ||
|
|
parse_currency(brex_account.currency) ||
|
|
"USD"
|
|
end
|
|
|
|
def transaction_amount_currency
|
|
amount_payload = data[:amount]
|
|
return nil unless amount_payload.is_a?(Hash)
|
|
|
|
amount_payload.with_indifferent_access[:currency]
|
|
end
|
|
|
|
def log_invalid_currency(currency_value)
|
|
Rails.logger.warn(
|
|
"Invalid Brex currency #{currency_value.inspect} for transaction #{data[:id].presence || 'unknown'} " \
|
|
"on brex_account #{brex_account.id} amount=#{data[:amount].inspect} account_currency=#{brex_account.currency.inspect}; defaulting to fallback"
|
|
)
|
|
end
|
|
|
|
def date
|
|
date_value = data[:posted_at_date].presence || data[:initiated_at_date].presence
|
|
|
|
case date_value
|
|
when String
|
|
Date.parse(date_value)
|
|
when Integer, Float
|
|
Time.at(date_value).to_date
|
|
when Time, DateTime
|
|
date_value.to_date
|
|
when Date
|
|
date_value
|
|
else
|
|
raise ArgumentError, "Invalid date format: #{date_value.inspect}"
|
|
end
|
|
rescue ArgumentError, TypeError => e
|
|
Rails.logger.error("Failed to parse Brex transaction date '#{date_value}': #{e.message}")
|
|
raise ArgumentError, "Unable to parse transaction date: #{date_value.inspect}"
|
|
end
|
|
|
|
def extra
|
|
{
|
|
brex: {
|
|
transaction_id: data[:id],
|
|
account_kind: brex_account.account_kind,
|
|
type: data[:type],
|
|
card_id: data[:card_id],
|
|
transfer_id: data[:transfer_id],
|
|
expense_id: data[:expense_id],
|
|
card_transaction_operation_reference_id: data[:card_transaction_operation_reference_id],
|
|
initiated_at_date: data[:initiated_at_date],
|
|
posted_at_date: data[:posted_at_date],
|
|
merchant: BrexAccount.sanitize_payload(data[:merchant])
|
|
}.compact
|
|
}
|
|
end
|
|
end
|