mirror of
https://github.com/we-promise/sure.git
synced 2026-08-12 11:10:20 +00:00
Adds YnabImport (mirroring ActualImport) for YNAB "Export budget" register CSVs: - Amount — combines the split Outflow/Inflow columns into a single signed amount (inflow - |outflow|), stripping currency symbols and thousands separators. A single signed Amount column takes precedence when present. - Category — resolves across export shapes: the combined "Category Group/Category" column, the split "Category Group" + "Category", or legacy YNAB 4 "Master Category" + "Sub Category". - Names — falls back from a blank Payee to the Memo, then the default row name. - Validation — requires at least one amount source (Outflow/Inflow or Amount); a file exposing none leaves rows un-clean instead of importing zero-dollar entries. Enables the previously-disabled YNAB option on the imports screen (using the YNAB logo, like Mint) with its configuration partial, and removes the now-dead imports.new.coming_soon locale key. Documents the type in the API import-type enums (rswag request spec + swagger_helper + generated openapi.yaml). Closes #1255.
135 lines
5.8 KiB
Ruby
135 lines
5.8 KiB
Ruby
module ImportsHelper
|
|
def mapping_label(mapping_class)
|
|
{
|
|
"Import::AccountTypeMapping" => I18n.t("imports.mapping_labels.account_type"),
|
|
"Import::AccountMapping" => I18n.t("imports.mapping_labels.account"),
|
|
"Import::CategoryMapping" => I18n.t("imports.mapping_labels.category"),
|
|
"Import::TagMapping" => I18n.t("imports.mapping_labels.tag")
|
|
}.fetch(mapping_class.name)
|
|
end
|
|
|
|
def import_col_label(key)
|
|
{
|
|
date: I18n.t("imports.column_labels.date"),
|
|
amount: I18n.t("imports.column_labels.amount"),
|
|
name: I18n.t("imports.column_labels.name"),
|
|
currency: I18n.t("imports.column_labels.currency"),
|
|
category: I18n.t("imports.column_labels.category"),
|
|
tags: I18n.t("imports.column_labels.tags"),
|
|
account: I18n.t("imports.column_labels.account"),
|
|
notes: I18n.t("imports.column_labels.notes"),
|
|
qty: I18n.t("imports.column_labels.qty"),
|
|
ticker: I18n.t("imports.column_labels.ticker"),
|
|
exchange: I18n.t("imports.column_labels.exchange"),
|
|
price: I18n.t("imports.column_labels.price"),
|
|
entity_type: I18n.t("imports.column_labels.entity_type"),
|
|
category_parent: I18n.t("imports.column_labels.category_parent"),
|
|
category_color: I18n.t("imports.column_labels.category_color"),
|
|
category_icon: I18n.t("imports.column_labels.category_icon"),
|
|
merchant_color: I18n.t("imports.column_labels.merchant_color"),
|
|
merchant_website: I18n.t("imports.column_labels.merchant_website")
|
|
}[key]
|
|
end
|
|
|
|
def dry_run_resource(key)
|
|
map = {
|
|
transactions: DryRunResource.new(label: t("imports.dry_run_resources.transactions"), icon: "credit-card", text_class: "text-info", bg_class: "bg-info/10"),
|
|
balances: DryRunResource.new(label: t("imports.dry_run_resources.balances"), icon: "line-chart", text_class: "text-secondary", bg_class: "bg-container-inset"),
|
|
accounts: DryRunResource.new(label: t("imports.dry_run_resources.accounts"), icon: "layers", text_class: "text-warning", bg_class: "bg-warning/10"),
|
|
categories: DryRunResource.new(label: t("imports.dry_run_resources.categories"), icon: "shapes", text_class: "text-info", bg_class: "bg-info/10"),
|
|
tags: DryRunResource.new(label: t("imports.dry_run_resources.tags"), icon: "tags", text_class: "text-info", bg_class: "bg-info/10"),
|
|
rules: DryRunResource.new(label: t("imports.dry_run_resources.rules"), icon: "workflow", text_class: "text-success", bg_class: "bg-success/10"),
|
|
merchants: DryRunResource.new(label: t("imports.dry_run_resources.merchants"), icon: "store", text_class: "text-warning", bg_class: "bg-warning/10"),
|
|
recurring_transactions: DryRunResource.new(label: t("imports.dry_run_resources.recurring_transactions"), icon: "repeat-2", text_class: "text-secondary", bg_class: "bg-container-inset"),
|
|
transfers: DryRunResource.new(label: t("imports.dry_run_resources.transfers"), icon: "repeat", text_class: "text-secondary", bg_class: "bg-container-inset"),
|
|
rejected_transfers: DryRunResource.new(label: t("imports.dry_run_resources.rejected_transfers"), icon: "ban", text_class: "text-destructive", bg_class: "bg-destructive/10"),
|
|
trades: DryRunResource.new(label: t("imports.dry_run_resources.trades"), icon: "arrow-left-right", text_class: "text-success", bg_class: "bg-success/10"),
|
|
holdings: DryRunResource.new(label: t("imports.dry_run_resources.holdings"), icon: "briefcase-business", text_class: "text-secondary", bg_class: "bg-container-inset"),
|
|
valuations: DryRunResource.new(label: t("imports.dry_run_resources.valuations"), icon: "trending-up", text_class: "text-info", bg_class: "bg-info/10"),
|
|
budgets: DryRunResource.new(label: t("imports.dry_run_resources.budgets"), icon: "wallet", text_class: "text-info", bg_class: "bg-info/10"),
|
|
budget_categories: DryRunResource.new(label: t("imports.dry_run_resources.budget_categories"), icon: "pie-chart", text_class: "text-success", bg_class: "bg-success/10")
|
|
}
|
|
|
|
map[key]
|
|
end
|
|
|
|
def import_verification_view(import)
|
|
ImportVerificationView.new(import.verification_payload)
|
|
end
|
|
|
|
def permitted_import_configuration_path(import)
|
|
if permitted_import_types.include?(import.type.underscore)
|
|
"import/configurations/#{import.type.underscore}"
|
|
else
|
|
raise "Unknown import type: #{import.type}"
|
|
end
|
|
end
|
|
|
|
def cell_class(row, field)
|
|
base = "bg-container text-sm focus:ring-gray-900 theme-dark:focus:ring-gray-100 focus:border-solid w-full max-w-full disabled:text-subdued"
|
|
|
|
row.valid? # populate errors
|
|
|
|
border = row.errors.key?(field) ? "border-destructive" : "border-transparent"
|
|
|
|
[ base, border ].join(" ")
|
|
end
|
|
|
|
def cell_is_valid?(row, field)
|
|
row.valid? # populate errors
|
|
!row.errors.key?(field)
|
|
end
|
|
|
|
private
|
|
def permitted_import_types
|
|
%w[transaction_import trade_import account_import mint_import actual_import ynab_import category_import rule_import merchant_import]
|
|
end
|
|
|
|
DryRunResource = Struct.new(:label, :icon, :text_class, :bg_class, keyword_init: true)
|
|
|
|
ImportVerificationView = Struct.new(:payload) do
|
|
def status
|
|
readback.fetch("status", "not_verified").to_s
|
|
end
|
|
|
|
def checked_total
|
|
checked_counts.values.sum(&:to_i)
|
|
end
|
|
|
|
def checked_counts
|
|
hash_value(readback["checked_counts"])
|
|
end
|
|
|
|
def mismatches
|
|
hash_value(readback["mismatches"])
|
|
end
|
|
|
|
def mismatches_count
|
|
mismatches.size
|
|
end
|
|
|
|
def mismatches_preview
|
|
mismatches.first(3)
|
|
end
|
|
|
|
def mismatches?
|
|
mismatches.any?
|
|
end
|
|
|
|
private
|
|
def readback
|
|
hash_value(payload_hash["readback"])
|
|
end
|
|
|
|
def payload_hash
|
|
hash_value(payload)
|
|
end
|
|
|
|
def hash_value(value)
|
|
return {} unless value.respond_to?(:to_h)
|
|
|
|
value.to_h.deep_stringify_keys
|
|
end
|
|
end
|
|
end
|