<% else %>
- <%= settings_section title: "Create Your API Key", subtitle: "Get programmatic access to your Maybe data" do %>
+
<% end %>
-<% if Current.user.admin? %>
- <%= settings_section title: "Data Import/Export" do %>
-
- <% end %>
-<% end %>
-
<%= settings_section title: t(".danger_zone_title") do %>
<% if Current.user.admin? %>
diff --git a/app/views/users/_user_menu.html.erb b/app/views/users/_user_menu.html.erb
index 1e46ee0c0..9a0d515a8 100644
--- a/app/views/users/_user_menu.html.erb
+++ b/app/views/users/_user_menu.html.erb
@@ -30,7 +30,7 @@
<% end %>
<% end %>
- <% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: settings_profile_path(return_to: request.fullpath)) %>
+ <% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: accounts_path(return_to: request.fullpath)) %>
<% menu.with_item(variant: "link", text: "Changelog", icon: "box", href: changelog_path) %>
<% if self_hosted? %>
diff --git a/config/locales/views/family_exports/en.yml b/config/locales/views/family_exports/en.yml
new file mode 100644
index 000000000..79674716b
--- /dev/null
+++ b/config/locales/views/family_exports/en.yml
@@ -0,0 +1,7 @@
+---
+en:
+ family_exports:
+ list:
+ in_progress: In progress
+ complete: Complete
+ failed: Failed
diff --git a/config/locales/views/family_exports/nb.yml b/config/locales/views/family_exports/nb.yml
new file mode 100644
index 000000000..be5e5b53e
--- /dev/null
+++ b/config/locales/views/family_exports/nb.yml
@@ -0,0 +1,7 @@
+---
+nb:
+ family_exports:
+ list:
+ in_progress: Pågår
+ complete: Fullført
+ failed: Mislykket
diff --git a/config/locales/views/family_exports/tr.yml b/config/locales/views/family_exports/tr.yml
new file mode 100644
index 000000000..db1da6c02
--- /dev/null
+++ b/config/locales/views/family_exports/tr.yml
@@ -0,0 +1,7 @@
+---
+tr:
+ family_exports:
+ list:
+ in_progress: Devam ediyor
+ complete: Tamamlandı
+ failed: Başarısız
diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml
index 972741933..e0aca782b 100644
--- a/config/locales/views/imports/en.yml
+++ b/config/locales/views/imports/en.yml
@@ -58,7 +58,7 @@ en:
imports:
empty:
message: No imports yet.
- new: New import
+ new: New Import
import:
complete: Complete
delete: Delete
@@ -71,8 +71,11 @@ en:
view: View
index:
imports: Imports
- new: New import
- title: Imports
+ new: New Import
+ title: Import/Export
+ exports: Exports
+ new_export: New Export
+ no_exports: No exports yet.
new:
description: You can manually import various types of data via CSV or use one
of our import templates like Mint.
diff --git a/config/locales/views/imports/nb.yml b/config/locales/views/imports/nb.yml
index 49cf223e3..29f15c58e 100644
--- a/config/locales/views/imports/nb.yml
+++ b/config/locales/views/imports/nb.yml
@@ -1,90 +1,93 @@
----
-nb:
- import:
- cleans:
- show:
- description: Rediger dataene dine i tabellen nedenfor. Røde celler er ugyldige.
- errors_notice: Du har feil i dataene dine. Hold musepekeren over feilen for å se
- detaljer.
- errors_notice_mobile: Du har feil i dataene dine. Trykk på feilverktøytipset for å se
- detaljer.
- title: Rengjør dataene dine
- configurations:
- mint_import:
- date_format_label: Datoformat
- show:
- description: Velg kolonnene som tilsvarer hvert felt i CSV-filen din.
- title: Konfigurer importen din
- trade_import:
- date_format_label: Datoformat
- transaction_import:
- date_format_label: Datoformat
- confirms:
- mappings:
- create_account: Opprett konto
- csv_mapping_label: "%{mapping} i CSV"
- maybe_mapping_label: "%{mapping} i Sure"
- no_accounts: Du har ingen kontoer ennå. Vennligst opprett en konto som
- vi kan bruke for (utilordnede) rader i CSV-filen din eller gå tilbake til Rengjør-trinnet
- og oppgi et kontonavn vi kan bruke.
- rows_label: Rader
- unassigned_account: Trenger å opprette en ny konto for utilordnede rader?
- show:
- account_mapping_description: Tilordne alle kontoene i den importerte filen din til
- Maybes eksisterende kontoer. Du kan også legge til nye kontoer eller la dem
- være ukategorisert.
- account_mapping_title: Tilordne kontoene dine
- account_type_mapping_description: Tilordne alle kontotypene i den importerte filen din til
- Maybes
- account_type_mapping_title: Tilordne kontotypene dine
- category_mapping_description: Tilordne alle kategoriene i den importerte filen din til
- Maybes eksisterende kategorier. Du kan også legge til nye kategorier eller la dem
- være ukategorisert.
- category_mapping_title: Tilordne kategoriene dine
- tag_mapping_description: Tilordne alle tagene i den importerte filen din til
- Maybes eksisterende tagger. Du kan også legge til nye tagger eller la dem
- være ukategorisert.
- tag_mapping_title: Tilordne tagene dine
- uploads:
- show:
- description: Lim inn eller last opp CSV-filen din nedenfor. Vennligst gjennomgå instruksjonene
- i tabellen nedenfor før du begynner.
- instructions_1: Nedenfor er et eksempel på CSV med kolonner tilgjengelig for import.
- instructions_2: CSV-filen din må ha en overskriftsrad
- instructions_3: Du kan navngi kolonnene dine hva du vil. Du vil tilordne
- dem på et senere trinn.
- instructions_4: Kolonner merket med en stjerne (*) er obligatoriske data.
- instructions_5: Ingen komma, ingen valutasymboler og ingen parenteser i tall.
- title: Importer dataene dine
- imports:
- empty:
- message: Ingen importer ennå.
- new: Ny import
- import:
- complete: Fullført
- delete: Slett
- failed: Mislykket
- in_progress: Pågår
- label: "%{type}: %{datetime}"
- revert_failed: Tilbakestilling mislykket
- reverting: Tilbakestiller
- uploading: Behandler rader
- view: Vis
- index:
- imports: Importer
- new: Ny import
- title: Importer
- new:
- description: Du kan manuelt importere ulike typer data via CSV eller bruke en av
- våre importmaler som Mint.
- import_accounts: Importer kontoer
- import_mint: Importer fra Mint
- import_portfolio: Importer investeringer
- import_transactions: Importer transaksjoner
- resume: Fortsett %{type}
- sources: Kilder
- title: Ny CSV-import
- ready:
- description: Her er en oppsummering av de nye elementene som vil bli lagt til kontoen din
- når du publiserer denne importen.
+---
+nb:
+ import:
+ cleans:
+ show:
+ description: Rediger dataene dine i tabellen nedenfor. Røde celler er ugyldige.
+ errors_notice: Du har feil i dataene dine. Hold musepekeren over feilen for å se
+ detaljer.
+ errors_notice_mobile: Du har feil i dataene dine. Trykk på feilverktøytipset for å se
+ detaljer.
+ title: Rengjør dataene dine
+ configurations:
+ mint_import:
+ date_format_label: Datoformat
+ show:
+ description: Velg kolonnene som tilsvarer hvert felt i CSV-filen din.
+ title: Konfigurer importen din
+ trade_import:
+ date_format_label: Datoformat
+ transaction_import:
+ date_format_label: Datoformat
+ confirms:
+ mappings:
+ create_account: Opprett konto
+ csv_mapping_label: "%{mapping} i CSV"
+ maybe_mapping_label: "%{mapping} i Sure"
+ no_accounts: Du har ingen kontoer ennå. Vennligst opprett en konto som
+ vi kan bruke for (utilordnede) rader i CSV-filen din eller gå tilbake til Rengjør-trinnet
+ og oppgi et kontonavn vi kan bruke.
+ rows_label: Rader
+ unassigned_account: Trenger å opprette en ny konto for utilordnede rader?
+ show:
+ account_mapping_description: Tilordne alle kontoene i den importerte filen din til
+ Maybes eksisterende kontoer. Du kan også legge til nye kontoer eller la dem
+ være ukategorisert.
+ account_mapping_title: Tilordne kontoene dine
+ account_type_mapping_description: Tilordne alle kontotypene i den importerte filen din til
+ Maybes
+ account_type_mapping_title: Tilordne kontotypene dine
+ category_mapping_description: Tilordne alle kategoriene i den importerte filen din til
+ Maybes eksisterende kategorier. Du kan også legge til nye kategorier eller la dem
+ være ukategorisert.
+ category_mapping_title: Tilordne kategoriene dine
+ tag_mapping_description: Tilordne alle tagene i den importerte filen din til
+ Maybes eksisterende tagger. Du kan også legge til nye tagger eller la dem
+ være ukategorisert.
+ tag_mapping_title: Tilordne tagene dine
+ uploads:
+ show:
+ description: Lim inn eller last opp CSV-filen din nedenfor. Vennligst gjennomgå instruksjonene
+ i tabellen nedenfor før du begynner.
+ instructions_1: Nedenfor er et eksempel på CSV med kolonner tilgjengelig for import.
+ instructions_2: CSV-filen din må ha en overskriftsrad
+ instructions_3: Du kan navngi kolonnene dine hva du vil. Du vil tilordne
+ dem på et senere trinn.
+ instructions_4: Kolonner merket med en stjerne (*) er obligatoriske data.
+ instructions_5: Ingen komma, ingen valutasymboler og ingen parenteser i tall.
+ title: Importer dataene dine
+ imports:
+ empty:
+ message: Ingen importer ennå.
+ new: Ny Import
+ import:
+ complete: Fullført
+ delete: Slett
+ failed: Mislykket
+ in_progress: Pågår
+ label: "%{type}: %{datetime}"
+ revert_failed: Tilbakestilling mislykket
+ reverting: Tilbakestiller
+ uploading: Behandler rader
+ view: Vis
+ index:
+ imports: Importer
+ new: Ny Import
+ title: Importer
+ exports: Eksporter
+ new_export: Ny Eksport
+ no_exports: Ingen eksporter ennå.
+ new:
+ description: Du kan manuelt importere ulike typer data via CSV eller bruke en av
+ våre importmaler som Mint.
+ import_accounts: Importer kontoer
+ import_mint: Importer fra Mint
+ import_portfolio: Importer investeringer
+ import_transactions: Importer transaksjoner
+ resume: Fortsett %{type}
+ sources: Kilder
+ title: Ny CSV-import
+ ready:
+ description: Her er en oppsummering av de nye elementene som vil bli lagt til kontoen din
+ når du publiserer denne importen.
title: Bekreft importdataene dine
\ No newline at end of file
diff --git a/config/locales/views/imports/tr.yml b/config/locales/views/imports/tr.yml
index 066f927d7..6ded70278 100644
--- a/config/locales/views/imports/tr.yml
+++ b/config/locales/views/imports/tr.yml
@@ -46,7 +46,7 @@ tr:
imports:
empty:
message: Henüz hiç içe aktarma yok.
- new: Yeni içe aktarma
+ new: Yeni İçe Aktarma
import:
complete: Tamamlandı
delete: Sil
@@ -59,8 +59,11 @@ tr:
view: Görüntüle
index:
imports: İçe aktarmalar
- new: Yeni içe aktarma
+ new: Yeni İçe Aktarma
title: İçe aktarmalar
+ exports: Dışa aktarmalar
+ new_export: Yeni Dışa Aktarma
+ no_exports: Henüz hiç dışa aktarma yok.
new:
description: Farklı veri türlerini CSV ile manuel olarak içe aktarabilir veya Mint gibi içe aktarma şablonlarımızı kullanabilirsiniz.
import_accounts: Hesapları içe aktar
diff --git a/config/locales/views/settings/api_keys/en.yml b/config/locales/views/settings/api_keys/en.yml
index b814e7427..05a526420 100644
--- a/config/locales/views/settings/api_keys/en.yml
+++ b/config/locales/views/settings/api_keys/en.yml
@@ -14,7 +14,7 @@ en:
show:
title: "API Key Management"
no_api_key:
- title: "Create Your API Key"
+ title: "API Key"
description: "Get programmatic access to your Maybe data with a secure API key."
what_you_can_do: "What you can do with the API:"
feature_1: "Access your account data programmatically"
diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml
index 8e7b17b8d..3c18e2f00 100644
--- a/config/locales/views/settings/en.yml
+++ b/config/locales/views/settings/en.yml
@@ -1,6 +1,20 @@
---
en:
settings:
+ ai_prompts:
+ show:
+ page_title: AI Prompts
+ openai_label: OpenAI
+ prompt_instructions: Prompt Instructions
+ main_system_prompt:
+ title: Main System Prompt
+ subtitle: Core instructions that define how the AI assistant behaves in all chat conversations
+ transaction_categorizer:
+ title: Transaction Categorizer
+ subtitle: AI automatically categorizes your transactions based on your defined categories
+ merchant_detector:
+ title: Merchant Detector
+ subtitle: AI identifies and enriches transaction data with merchant information
billings:
show:
page_title: Billing
@@ -59,10 +73,10 @@ en:
invitation_link: Invitation link
invite_member: Add member
last_name: Last Name
- page_title: Account
+ page_title: Profile Info
pending: Pending
profile_subtitle: Customize how you appear on Sure
- profile_title: Profile
+ profile_title: Personal
remove_invitation: Remove Invitation
remove_member: Remove Member
save: Save
@@ -71,24 +85,27 @@ en:
page_title: Security
settings_nav:
accounts_label: Accounts
+ advanced_section_title: Advanced
+ ai_prompts_label: AI Prompts
api_key_label: API Key
billing_label: Billing
categories_label: Categories
feedback_label: Feedback
general_section_title: General
- imports_label: Imports
+ imports_label: Import/Export
logout: Logout
merchants_label: Merchants
+ guides_label: Guides
other_section_title: More
preferences_label: Preferences
- profile_label: Account
+ profile_label: Profile Info
rules_label: Rules
security_label: Security
self_hosting_label: Self-Hosting
tags_label: Tags
transactions_section_title: Transactions
whats_new_label: What's new
- api_keys_label: API Keys
+ api_keys_label: API Key
bank_sync_label: Bank Sync
settings_nav_link_large:
next: Next
diff --git a/config/locales/views/settings/nb.yml b/config/locales/views/settings/nb.yml
index e68b80fa6..0ffe61fed 100644
--- a/config/locales/views/settings/nb.yml
+++ b/config/locales/views/settings/nb.yml
@@ -1,96 +1,97 @@
----
-nb:
- settings:
- billings:
- show:
- page_title: Fakturering
- subscription_subtitle: Oppdater abonnementet og faktureringsdetaljene dine
- subscription_title: Administrer abonnement
- preferences:
- show:
- country: Land
- currency: Valuta
- date_format: Datoformat
- general_subtitle: Konfigurer preferansene dine
- general_title: Generelt
- default_period: Standardperiode
- language: Språk
- page_title: Preferanser
- theme_dark: Mørk
- theme_light: Lys
- theme_subtitle: Velg et foretrukket tema for appen
- theme_system: System
- theme_title: Tema
- timezone: Tidssone
- profiles:
- destroy:
- cannot_remove_self: Du kan ikke fjerne deg selv fra din egen konto.
- member_removal_failed: Det oppsto et problem med å fjerne medlemmet.
- member_removed: Medlemmet ble fjernet vellykket.
- not_authorized: Du er ikke autorisert til å fjerne medlemmer.
- show:
- confirm_delete:
- body: Er du sikker på at du vil permanent slette kontoen din? Denne handlingen er irreversibel.
- title: Slett konto?
- confirm_reset:
- body: Er du sikker på at du vil tilbakestille kontoen din? Dette vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data. Denne handlingen kan ikke angres.
- title: Tilbakestill konto?
- confirm_remove_invitation:
- body: Er du sikker på at du vil fjerne invitasjonen for %{email}?
- title: Fjern invitasjon
- confirm_remove_member:
- body: Er du sikker på at du vil fjerne %{name} fra kontoen din?
- title: Fjern medlem
- danger_zone_title: Fareområde
- delete_account: Slett konto
- delete_account_warning: Sletting av kontoen din vil permanent fjerne alle
- dataene dine og kan ikke angres.
- reset_account: Tilbakestill konto
- reset_account_warning: Tilbakestilling av kontoen din vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data, men beholde brukerkontoen din intakt.
- email: E-post
- first_name: Fornavn
- household_form_input_placeholder: Angi husholdningsnavn
- household_form_label: Husholdningsnavn
- household_subtitle: Inviter familiemedlemmer, partnere og andre individer. Inviterte
- kan logge inn på husholdningen din og få tilgang til dine delte kontoer.
- household_title: Husholdning
- invitation_link: Invitasjonslenke
- invite_member: Legg til medlem
- last_name: Etternavn
- page_title: Konto
- pending: Venter
- profile_subtitle: Tilpass hvordan du vises på Sure
- profile_title: Profil
- remove_invitation: Fjern invitasjon
- remove_member: Fjern medlem
- save: Lagre
- securities:
- show:
- page_title: Sikkerhet
- settings_nav:
- accounts_label: Kontoer
- api_key_label: API-nøkkel
- billing_label: Fakturering
- categories_label: Kategorier
- feedback_label: Tilbakemelding
- general_section_title: Generelt
- imports_label: Importer
- logout: Logg ut
- merchants_label: Forhandlere
- other_section_title: Mer
- preferences_label: Preferanser
- profile_label: Konto
- rules_label: Regler
- security_label: Sikkerhet
- self_hosting_label: Selvhosting
- tags_label: Tagger
- transactions_section_title: Transaksjoner
- whats_new_label: Hva er nytt
- settings_nav_link_large:
- next: Neste
- previous: Tilbake
- user_avatar_field:
- accepted_formats: JPG eller PNG. 5MB maks.
- choose: Last opp bilde
- choose_label: (valgfritt)
+---
+nb:
+ settings:
+ billings:
+ show:
+ page_title: Fakturering
+ subscription_subtitle: Oppdater abonnementet og faktureringsdetaljene dine
+ subscription_title: Administrer abonnement
+ preferences:
+ show:
+ country: Land
+ currency: Valuta
+ date_format: Datoformat
+ general_subtitle: Konfigurer preferansene dine
+ general_title: Generelt
+ default_period: Standardperiode
+ language: Språk
+ page_title: Preferanser
+ theme_dark: Mørk
+ theme_light: Lys
+ theme_subtitle: Velg et foretrukket tema for appen
+ theme_system: System
+ theme_title: Tema
+ timezone: Tidssone
+ profiles:
+ destroy:
+ cannot_remove_self: Du kan ikke fjerne deg selv fra din egen konto.
+ member_removal_failed: Det oppsto et problem med å fjerne medlemmet.
+ member_removed: Medlemmet ble fjernet vellykket.
+ not_authorized: Du er ikke autorisert til å fjerne medlemmer.
+ show:
+ confirm_delete:
+ body: Er du sikker på at du vil permanent slette kontoen din? Denne handlingen er irreversibel.
+ title: Slett konto?
+ confirm_reset:
+ body: Er du sikker på at du vil tilbakestille kontoen din? Dette vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data. Denne handlingen kan ikke angres.
+ title: Tilbakestill konto?
+ confirm_remove_invitation:
+ body: Er du sikker på at du vil fjerne invitasjonen for %{email}?
+ title: Fjern invitasjon
+ confirm_remove_member:
+ body: Er du sikker på at du vil fjerne %{name} fra kontoen din?
+ title: Fjern medlem
+ danger_zone_title: Fareområde
+ delete_account: Slett konto
+ delete_account_warning: Sletting av kontoen din vil permanent fjerne alle
+ dataene dine og kan ikke angres.
+ reset_account: Tilbakestill konto
+ reset_account_warning: Tilbakestilling av kontoen din vil slette alle kontoene dine, kategorier, forhandlere, tagger og andre data, men beholde brukerkontoen din intakt.
+ email: E-post
+ first_name: Fornavn
+ household_form_input_placeholder: Angi husholdningsnavn
+ household_form_label: Husholdningsnavn
+ household_subtitle: Inviter familiemedlemmer, partnere og andre individer. Inviterte
+ kan logge inn på husholdningen din og få tilgang til dine delte kontoer.
+ household_title: Husholdning
+ invitation_link: Invitasjonslenke
+ invite_member: Legg til medlem
+ last_name: Etternavn
+ page_title: Konto
+ pending: Venter
+ profile_subtitle: Tilpass hvordan du vises på Sure
+ profile_title: Profil
+ remove_invitation: Fjern invitasjon
+ remove_member: Fjern medlem
+ save: Lagre
+ securities:
+ show:
+ page_title: Sikkerhet
+ settings_nav:
+ accounts_label: Kontoer
+ api_key_label: API-nøkkel
+ advanced_section_title: Avansert
+ billing_label: Fakturering
+ categories_label: Kategorier
+ feedback_label: Tilbakemelding
+ general_section_title: Generelt
+ imports_label: Importer
+ logout: Logg ut
+ merchants_label: Forhandlere
+ other_section_title: Mer
+ preferences_label: Preferanser
+ profile_label: Konto
+ rules_label: Regler
+ security_label: Sikkerhet
+ self_hosting_label: Selvhosting
+ tags_label: Tagger
+ transactions_section_title: Transaksjoner
+ whats_new_label: Hva er nytt
+ settings_nav_link_large:
+ next: Neste
+ previous: Tilbake
+ user_avatar_field:
+ accepted_formats: JPG eller PNG. 5MB maks.
+ choose: Last opp bilde
+ choose_label: (valgfritt)
change: Endre bilde
\ No newline at end of file
diff --git a/config/locales/views/settings/tr.yml b/config/locales/views/settings/tr.yml
index fa568c33f..603f020ef 100644
--- a/config/locales/views/settings/tr.yml
+++ b/config/locales/views/settings/tr.yml
@@ -68,6 +68,7 @@ tr:
settings_nav:
accounts_label: Hesaplar
api_key_label: API Anahtarı
+ advanced_section_title: Gelişmiş
billing_label: Faturalandırma
categories_label: Kategoriler
feedback_label: Geri Bildirim
diff --git a/config/routes.rb b/config/routes.rb
index d89dcdb47..9e4e80548 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -24,7 +24,7 @@ Rails.application.routes.draw do
end
end
- resources :family_exports, only: %i[new create index] do
+ resources :family_exports, only: %i[new create index destroy] do
member do
get :download
end
@@ -63,6 +63,8 @@ Rails.application.routes.draw do
resource :billing, only: :show
resource :security, only: :show
resource :api_key, only: [ :show, :new, :create, :destroy ]
+ resource :ai_prompts, only: :show
+ resource :guides, only: :show
resource :bank_sync, only: :show, controller: "bank_sync"
end
diff --git a/docs/onboarding/guide.md b/docs/onboarding/guide.md
new file mode 100644
index 000000000..6b4973de6
--- /dev/null
+++ b/docs/onboarding/guide.md
@@ -0,0 +1,160 @@
+# Welcome to Sure!
+
+This guide aims to assist new users through:
+
+1. Creating a Sure account
+2. Adding your first accounts
+3. Recording transactions
+
+This guide also covers the differences between **asset** and **liability** accounts, a key concept for using and understanding balances in Sure!
+
+> [!IMPORTANT]
+> Sure is evolving quickly. If you find something inaccurate while following this guide, please:
+>
+> - Ask in the [Discord](https://discord.gg/36ZGBsxYEK)
+> - Open an [issue](https://github.com/we-promise/sure/issues/new/choose)
+> - Or if you know the answer, open a [PR](https://github.com/we-promise/sure/compare)!
+
+
+## 1. Creating your Sure Account
+
+Once Sure is installed, open a browser and navigate to [localhost:3000](http://localhost:3000/sessions/new).
+You will see the **login page** (pictured below). Since we do not have an account yet, click on **Sign Up** to begin.
+
+

+
+
+
+You’ll be guided through a short series of screens to set your **login details**, **personal information**, and **preferences**.
+When you arrive at the main dashboard, showing **No accounts yet**, you’re all set up!
+
+

+
+
+
+> [!Note]
+> The next sections of this guide cover how to **manually add accounts and transactions** in Sure.
+> If you’d like to use an integration with a data provider instead, see:
+>
+> - **Lunch Flow** (WIP)
+> - [**Plaid**](/docs/hosting/plaid.md)
+> - **SimpleFin** (WIP)
+>
+> Even if you use an integration, we still recommend reading through this guide to understand **account types** and how they work in Sure.
+
+
+## 2. Account Types in Sure
+
+Sure supports several account types, which are grouped into **Assets** (things you own) and **Debts/Liabilities** (things you owe):
+
+| Assets | Debts/Liabilities |
+| ----------- | ----------------- |
+| Cash | Credit Card |
+| Investment | Loan |
+| Crypto | Other Liability |
+| Property | |
+| Vehicle | |
+| Other Asset | |
+
+
+## 3. How Asset Accounts Work
+
+Cash, checking and savings accounts **increase** when you add money and **decrease** when you spend money.
+
+Example:
+
+- Starting balance: $500
+- Add an expense of $20 -> balance is now $480
+- Add an income of $100 -> balance is now $580
+
+
+## 4. How Debt Accounts Work (Liabilities)
+
+Liability accounts track how much money you **owe**, so the math can feel *backwards* compared to an asset account.
+
+**Key rule:**
+
+- **Positive Balances** = you owe money
+- **Negative balances** = the bank owes *you* (e.g. overpayment or refund)
+
+**Transactions behave like this:**
+
+- **Expenses** (e.g. purchases) => increase your debt (you owe more)
+- **Payments or refunds** => decrease your debt (you owe less)
+
+Credit Card example:
+
+1. Balance: **$200 owed**
+2. Spend $20 => You now owe $220 (balance goes *up* in red)
+3. Pay off $50 => You now owe $170 (balance goes *down* in green)
+
+Overpayment Example:
+
+1. Balance: -$44 (bank owes you $44)
+2. Spend $1 => Bank now owes you **$43** (balance shown as -$43, moving towards zero)
+
+> [!TIP]
+> Why does it work this way? This matches standard accounting and what your credit card provider shows online. Think of a liability balance as "**Amount Owed**", not "available cash."
+
+
+## 5. Quick Reference: Assets vs. Liability Behavior
+
+| Action | Asset Account (e.g. Checking) | Liability Account (e.g. Credit Card) |
+| ---------------- | ----------------------------- | ------------------------------------ |
+| Spend $20 | Balance ↓ $20 | Balance ↑ $20 (more debt) |
+| Receive $50 | Balance ↑ $50 | Balance ↓ $50 (less debt) |
+| Negative Balance | Overdraft | Bank owes *you* money |
+
+
+## 6. Adding Accounts
+
+For this example we'll add a **Savings Account**.
+
+>[!TIP]
+>If you’re adding a **credit card**, **loan**, or any other **debt**, be sure to select a **Credit Card** or **Liability** account type instead of **Cash**. This will ensure balances update correctly and match what your bank shows.
+
+Most bank accounts (checking, savings, money market) are **Cash Accounts**
+1. Click **+ Add Account** → **Cash** → **Enter Account Balance**
+2. Fill in details such as:
+ - Account name
+ - Current Balance
+ - Account Subtype (This is where you specify checking, savings, or other)
+3. Click **Create Account** when you are ready to proceed.
+
+

+
+
+
+Once created, you'll return to the **Home** screen.
+You'll now see:
+- Your new cash account in the **Accounts** list (left side)
+- An overview of your accounts in the center, under the net worth bar.
+
+To get this bar moving let's add some transactions!
+
+

+
+## 7. Adding Transactions
+
+To add a transaction:
+1. Go to the **Transactions** page (left sidebar, under **Home**, above **Budgets**)
+2. Click **+ New Transaction** (top right)
+3. Choose the transaction type:
+ - **Expense** → Spending money
+ - **Income** → Receiving money
+ - **Transfer** → Move money between accounts
+4. Enter the details, then click **Add transaction**
+
+You will now see the transaction you added in your **transaction history**, as well as the **net worth chart** updating accordingly.
+
+

+
+## 8. Next Steps
+
+Now that you have one account and your first transaction:
+- Explore the other account types that Sure offers, adding ones relevant to your finances.
+- **Categorize** and **Tag** transactions for better searching and reporting.
+- Experiment with **Budgets** to track your spending habits.
+- If you have many historical transactions, use **Bulk Import** to load them in.
+
+More detailed user guides for these features are coming soon™.
diff --git a/test/controllers/family_exports_controller_test.rb b/test/controllers/family_exports_controller_test.rb
index 63adf7884..017509bba 100644
--- a/test/controllers/family_exports_controller_test.rb
+++ b/test/controllers/family_exports_controller_test.rb
@@ -33,7 +33,7 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
post family_exports_path
end
- assert_redirected_to settings_profile_path
+ assert_redirected_to imports_path
assert_equal "Export started. You'll be able to download it shortly.", flash[:notice]
export = @family.family_exports.last
@@ -67,7 +67,87 @@ class FamilyExportsControllerTest < ActionDispatch::IntegrationTest
export = @family.family_exports.create!(status: "processing")
get download_family_export_path(export)
- assert_redirected_to settings_profile_path
+ assert_redirected_to imports_path
assert_equal "Export not ready for download", flash[:alert]
end
+
+ test "admin can delete export" do
+ export = @family.family_exports.create!(status: "completed")
+
+ assert_difference "@family.family_exports.count", -1 do
+ delete family_export_path(export)
+ end
+
+ assert_redirected_to imports_path
+ assert_equal "Export deleted successfully", flash[:notice]
+ end
+
+ test "admin can delete export with attached file" do
+ export = @family.family_exports.create!(status: "completed")
+ export.export_file.attach(
+ io: StringIO.new("test zip content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ assert export.export_file.attached?
+ assert_difference "@family.family_exports.count", -1 do
+ delete family_export_path(export)
+ end
+
+ assert_redirected_to imports_path
+ assert_equal "Export deleted successfully", flash[:notice]
+ end
+
+ test "admin can delete failed export with attached file" do
+ export = @family.family_exports.create!(status: "failed")
+ export.export_file.attach(
+ io: StringIO.new("failed export content"),
+ filename: "failed.zip",
+ content_type: "application/zip"
+ )
+
+ assert export.export_file.attached?
+ assert_difference "@family.family_exports.count", -1 do
+ delete family_export_path(export)
+ end
+
+ assert_redirected_to imports_path
+ assert_equal "Export deleted successfully", flash[:notice]
+ end
+
+ test "export file is purged when export is deleted" do
+ export = @family.family_exports.create!(status: "completed")
+ export.export_file.attach(
+ io: StringIO.new("test zip content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ # Verify file is attached
+ assert export.export_file.attached?
+ file_id = export.export_file.id
+
+ # Delete the export
+ delete family_export_path(export)
+
+ # Verify the export record is gone
+ assert_not FamilyExport.exists?(export.id)
+
+ # Verify the Active Storage attachment is also gone
+ # Note: Active Storage purges files asynchronously with `dependent: :purge_later`
+ # In tests, we can check that the attachment record is gone
+ assert_not ActiveStorage::Attachment.exists?(file_id)
+ end
+
+ test "non-admin cannot delete export" do
+ export = @family.family_exports.create!(status: "completed")
+ sign_in @non_admin
+
+ assert_no_difference "@family.family_exports.count" do
+ delete family_export_path(export)
+ end
+
+ assert_redirected_to root_path
+ end
end
diff --git a/test/models/family_export_test.rb b/test/models/family_export_test.rb
index 45420adf3..3cd3abb3d 100644
--- a/test/models/family_export_test.rb
+++ b/test/models/family_export_test.rb
@@ -1,7 +1,131 @@
require "test_helper"
class FamilyExportTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
+ setup do
+ @family = families(:dylan_family)
+ @export = @family.family_exports.create!
+ end
+
+ test "belongs to family" do
+ assert_equal @family, @export.family
+ end
+
+ test "has default status of pending" do
+ assert_equal "pending", @export.status
+ end
+
+ test "can have export file attached" do
+ @export.export_file.attach(
+ io: StringIO.new("test content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ assert @export.export_file.attached?
+ assert_equal "test.zip", @export.export_file.filename.to_s
+ assert_equal "application/zip", @export.export_file.content_type
+ end
+
+ test "filename is generated correctly" do
+ travel_to Time.zone.local(2024, 1, 15, 14, 30, 0) do
+ export = @family.family_exports.create!
+ expected_filename = "maybe_export_20240115_143000.zip"
+ assert_equal expected_filename, export.filename
+ end
+ end
+
+ test "downloadable? returns true for completed export with file" do
+ @export.update!(status: "completed")
+ @export.export_file.attach(
+ io: StringIO.new("test content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ assert @export.downloadable?
+ end
+
+ test "downloadable? returns false for pending export" do
+ @export.update!(status: "pending")
+ @export.export_file.attach(
+ io: StringIO.new("test content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ assert_not @export.downloadable?
+ end
+
+ test "downloadable? returns false for completed export without file" do
+ @export.update!(status: "completed")
+
+ assert_not @export.downloadable?
+ end
+
+ test "downloadable? returns false for failed export with file" do
+ @export.update!(status: "failed")
+ @export.export_file.attach(
+ io: StringIO.new("test content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ assert_not @export.downloadable?
+ end
+
+ test "export file is purged when export is destroyed" do
+ @export.export_file.attach(
+ io: StringIO.new("test content"),
+ filename: "test.zip",
+ content_type: "application/zip"
+ )
+
+ # Verify file is attached
+ assert @export.export_file.attached?
+ file_id = @export.export_file.id
+ blob_id = @export.export_file.blob.id
+
+ # Destroy the export
+ @export.destroy!
+
+ # Verify the export record is gone
+ assert_not FamilyExport.exists?(@export.id)
+
+ # Verify the Active Storage attachment is gone
+ assert_not ActiveStorage::Attachment.exists?(file_id)
+
+ # Note: Active Storage purges blobs asynchronously with dependent: :purge_later
+ # In tests, we can verify the attachment is gone, which is the immediate effect
+ # The blob will be purged in the background
+ end
+
+ test "can transition through statuses" do
+ assert_equal "pending", @export.status
+
+ @export.processing!
+ assert_equal "processing", @export.status
+
+ @export.completed!
+ assert_equal "completed", @export.status
+
+ @export.failed!
+ assert_equal "failed", @export.status
+ end
+
+ test "ordered scope returns exports in descending order" do
+ # Clear existing exports to avoid interference
+ @family.family_exports.destroy_all
+
+ # Create exports with specific timestamps
+ old_export = @family.family_exports.create!
+ old_export.update_column(:created_at, 2.days.ago)
+
+ new_export = @family.family_exports.create!
+ new_export.update_column(:created_at, 1.day.ago)
+
+ ordered_exports = @family.family_exports.ordered.to_a
+ assert_equal 2, ordered_exports.length
+ assert_equal new_export.id, ordered_exports.first.id
+ assert_equal old_export.id, ordered_exports.last.id
+ end
end
diff --git a/test/system/settings/api_keys_test.rb b/test/system/settings/api_keys_test.rb
index d6beeeee7..e558cb945 100644
--- a/test/system/settings/api_keys_test.rb
+++ b/test/system/settings/api_keys_test.rb
@@ -10,10 +10,9 @@ class Settings::ApiKeysTest < ApplicationSystemTestCase
test "should show no API key state when user has no active keys" do
visit settings_api_key_path
- assert_text "Create Your API Key"
- assert_text "Get programmatic access to your Maybe data"
- assert_text "Access your account data programmatically"
+ assert_text "API Key"
assert_link "Create API Key", href: new_settings_api_key_path
+ assert_text "Access your account data programmatically"
end
test "should navigate to create new API key form" do
@@ -33,7 +32,7 @@ class Settings::ApiKeysTest < ApplicationSystemTestCase
fill_in "API Key Name", with: "Test Integration Key"
choose "Read/Write"
- click_button "Create API Key"
+ click_button "Save API Key"
# Should redirect to show page with the API key details
assert_current_path settings_api_key_path
@@ -100,7 +99,7 @@ class Settings::ApiKeysTest < ApplicationSystemTestCase
fill_in "API Key Name", with: "New API Key"
choose "Read Only"
- click_button "Create API Key"
+ click_button "Save API Key"
# Should redirect to show page with new key
assert_text "New API Key"
@@ -133,8 +132,8 @@ class Settings::ApiKeysTest < ApplicationSystemTestCase
# Wait for redirect after revoke
assert_no_selector "#confirm-dialog"
- assert_text "Create Your API Key"
- assert_text "Get programmatic access to your Maybe data"
+ assert_text "API Key"
+ assert_text "Access your account data programmatically"
# Key should be revoked in the database
api_key.reload
@@ -167,7 +166,7 @@ class Settings::ApiKeysTest < ApplicationSystemTestCase
# Try to submit without name
choose "Read Only"
- click_button "Create API Key"
+ click_button "Save API Key"
# Should stay on form with validation error
assert_current_path new_settings_api_key_path
diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb
index c7d5efe44..0271f129d 100644
--- a/test/system/settings_test.rb
+++ b/test/system/settings_test.rb
@@ -5,13 +5,18 @@ class SettingsTest < ApplicationSystemTestCase
sign_in @user = users(:family_admin)
@settings_links = [
- [ "Account", settings_profile_path ],
- [ "Preferences", settings_preferences_path ],
[ "Accounts", accounts_path ],
- [ "Tags", tags_path ],
+ [ "Bank Sync", settings_bank_sync_path ],
+ [ "Preferences", settings_preferences_path ],
+ [ "Profile Info", settings_profile_path ],
+ [ "Security", settings_security_path ],
[ "Categories", categories_path ],
+ [ "Tags", tags_path ],
+ [ "Rules", rules_path ],
[ "Merchants", family_merchants_path ],
- [ "Imports", imports_path ],
+ [ "AI Prompts", settings_ai_prompts_path ],
+ [ "API Key", settings_api_key_path ],
+ [ "Guides", settings_guides_path ],
[ "What's new", changelog_path ],
[ "Feedback", feedback_path ]
]
@@ -20,8 +25,8 @@ class SettingsTest < ApplicationSystemTestCase
test "can access settings from sidebar" do
VCR.use_cassette("git_repository_provider/fetch_latest_release_notes") do
open_settings_from_sidebar
- assert_selector "h1", text: "Account"
- assert_current_path settings_profile_path, ignore_query: true
+ assert_selector "h1", text: "Accounts"
+ assert_current_path accounts_path, ignore_query: true
@settings_links.each do |name, path|
click_link name