Compare commits

...

132 Commits

Author SHA1 Message Date
Zach Gollwitzer
3f9858a67f Add streaming chat 2025-03-12 14:06:42 -04:00
Zach Gollwitzer
d1b83541c1 AI chat layout skeleton 2025-03-12 12:39:16 -04:00
Zach Gollwitzer
dd75cadebc Fix transaction filters when transfers are present (#1986)
* Proper filtering of transfers in search

* Fix transaction search
2025-03-11 15:38:45 -04:00
Josh Pigford
ed55ef624b Update billing settings view and locales 2025-03-11 13:00:34 -05:00
Zach Gollwitzer
f363fd4a4e Fix incorrect totals calculation when family has loan payments (#1984)
* Fix income totals calculation error when loan payments exist

* Include transaction totals in totals query
2025-03-11 12:37:57 -04:00
Zach Gollwitzer
b8a3ca7732 Fetch exchange rates for accounts that require conversion for net worth rollups (#1983)
* Sync required exchange rates for accounts

* Refactor into concern
2025-03-11 10:10:28 -04:00
Zach Gollwitzer
7b751ac7ca Do not prompt upgrades until user is logged in
Fixes #1982
2025-03-11 09:11:40 -04:00
Josh Pigford
15d59959cf Fix issue of syncing notice covering up user menu 2025-03-10 11:20:58 -05:00
dependabot[bot]
c66401dc0f Bump stripe from 13.4.1 to 13.5.0 (#1970)
Bumps [stripe](https://github.com/stripe/stripe-ruby) from 13.4.1 to 13.5.0.
- [Release notes](https://github.com/stripe/stripe-ruby/releases)
- [Changelog](https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-ruby/compare/v13.4.1...v13.5.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:34:15 -04:00
dependabot[bot]
9dcb9e8ed2 Bump webmock from 3.25.0 to 3.25.1 (#1968)
Bumps [webmock](https://github.com/bblimke/webmock) from 3.25.0 to 3.25.1.
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.25.0...v3.25.1)

---
updated-dependencies:
- dependency-name: webmock
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:16:01 -04:00
dependabot[bot]
045fa1931c Bump good_job from 4.9.0 to 4.9.3 (#1969)
Bumps [good_job](https://github.com/bensheldon/good_job) from 4.9.0 to 4.9.3.
- [Release notes](https://github.com/bensheldon/good_job/releases)
- [Changelog](https://github.com/bensheldon/good_job/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bensheldon/good_job/compare/v4.9.0...v4.9.3)

---
updated-dependencies:
- dependency-name: good_job
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:15:51 -04:00
dependabot[bot]
3f8351abfe Bump rubocop-rails-omakase from 1.0.0 to 1.1.0 (#1971)
Bumps [rubocop-rails-omakase](https://github.com/rails/rubocop-rails-omakase) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/rails/rubocop-rails-omakase/releases)
- [Commits](https://github.com/rails/rubocop-rails-omakase/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: rubocop-rails-omakase
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:38 -04:00
dependabot[bot]
dc44da6c00 Bump redcarpet from 3.6.0 to 3.6.1 (#1972)
Bumps [redcarpet](https://github.com/vmg/redcarpet) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/vmg/redcarpet/releases)
- [Changelog](https://github.com/vmg/redcarpet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vmg/redcarpet/compare/v3.6.0...v3.6.1)

---
updated-dependencies:
- dependency-name: redcarpet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:25 -04:00
dependabot[bot]
2e4180fbf0 Bump turbo-rails from 2.0.11 to 2.0.13 (#1973)
Bumps [turbo-rails](https://github.com/hotwired/turbo-rails) from 2.0.11 to 2.0.13.
- [Release notes](https://github.com/hotwired/turbo-rails/releases)
- [Commits](https://github.com/hotwired/turbo-rails/compare/v2.0.11...v2.0.13)

---
updated-dependencies:
- dependency-name: turbo-rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:15 -04:00
dependabot[bot]
4b19ca50eb Bump i18n-tasks from 1.0.14 to 1.0.15 (#1974)
Bumps [i18n-tasks](https://github.com/glebm/i18n-tasks) from 1.0.14 to 1.0.15.
- [Release notes](https://github.com/glebm/i18n-tasks/releases)
- [Changelog](https://github.com/glebm/i18n-tasks/blob/main/CHANGES.md)
- [Commits](https://github.com/glebm/i18n-tasks/compare/v1.0.14...v1.0.15)

---
updated-dependencies:
- dependency-name: i18n-tasks
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:13:05 -04:00
Zach Gollwitzer
a3cd5f4f1d Format money for trade history in holdings drawer (#1961)
* Format money for trade history in holdings drawer

* Fix broken tests

* Lint fix
2025-03-07 19:09:54 -05:00
Zach Gollwitzer
86bf47a32e Ensure holdings are normalized to account currency 2025-03-07 18:02:08 -05:00
Zach Gollwitzer
5f8a3c9f50 Search securities with correct exchange mic 2025-03-07 17:48:26 -05:00
Zach Gollwitzer
eac5d5e663 Populate holdings for "offline" securities properly (#1958)
* Placeholder logic for missing prices

* Generate holdings properly for "offline" securities

* Separate forward and reverse calculators for holdings and balances

* Remove unnecessary currency conversion during sync

* Clearer sync process

* Move price caching logic to dedicated model

* Base holding calculator

* Base calculator for balances

* Finish balance calculators

* Better naming

* Logs cleanup

* Remove stale data type

* Remove stale test

* Fix price lookup logic for holdings sync

* Fix Plaid item sync regression

* Remove temp logging

* Calculate cash and holdings series

* Add holdings, cash, and balance series dropdown for investments
2025-03-07 17:35:55 -05:00
Nikhil Badyal
26762477a3 Preference to set default_period (#1941) 2025-03-07 10:05:54 -05:00
Zach Gollwitzer
372b64ffea Fix Plaid sync error when current balance is null 2025-03-05 16:02:07 -05:00
Zach Gollwitzer
9627a6bf6f Add tagged logging to sync process (#1956)
* Add tagged logging to sync process

* Reduce logging in syncer

* Typo
2025-03-05 15:38:31 -05:00
Josh Pigford
cffafd23f0 Logger cleanup 2025-03-05 13:44:56 -06:00
Josh Pigford
f7fa8fa085 Disable turbo on login forms 2025-03-05 13:32:53 -06:00
Josh Pigford
28bfcda50a Temporary additional logging to continue debugging MFA issues 2025-03-05 13:20:36 -06:00
Josh Pigford
e49bda4a2e Another attempt at fixing MFA issues 2025-03-05 13:10:53 -06:00
Josh Pigford
071ad52c7f Potential fix for MFA login issues 2025-03-05 13:04:45 -06:00
Zach Gollwitzer
381e39bea8 Fix: Purge stale holdings from accounts during sync (#1954)
* Fix: Purge stale holdings from accounts during sync

* Fix typo

* Prevent Plaid holding deletions
2025-03-05 12:21:17 -05:00
Zach Gollwitzer
eaa1b6abe0 Update issue templates 2025-03-05 11:01:07 -05:00
Zach Gollwitzer
e384369cfb Adjust graph intervals to show more data
Fixes #1948
2025-03-05 10:20:02 -05:00
Zach Gollwitzer
8d0509fda0 Conditionally show commit sha
Fixes #1951
2025-03-05 10:15:12 -05:00
Zach Gollwitzer
d66c37939a Update bug_report.md
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-03-05 10:07:29 -05:00
Zach Gollwitzer
cf59fe45e7 Fix ticker filling when Synth is connected (#1950) 2025-03-05 09:30:47 -05:00
Zach Gollwitzer
0544089710 Account-level import configuration templates (#1946)
* Account-level import configuration templates

* Default import to family's preferred date format
2025-03-04 13:10:01 -05:00
Zach Gollwitzer
5b2fa3d707 Fix commit resolution for Docker builds 2025-03-04 07:50:21 -05:00
Bryan McKnight
cf0e573533 Fix modal closing on color picker drag #1869 (#1931)
* Replaced data-action click event with data-action mousedown to prevent the modal from hiding on mouse up whenever mouse down starts within the modal

* Changed click events to mousedown within dialog elements to trigger the closing of the element
2025-03-03 16:37:12 -05:00
Zach Gollwitzer
4e96ca8376 Add manual Docker publishing trigger in GH action workflow 2025-03-03 14:34:56 -05:00
Zach Gollwitzer
c5da8ea550 Allow CSV imports to be configured with single or multi-account mode (#1943)
* Allow CSV imports to be configured to a single account or multiple accounts

* Initialize import directly from accounts page

* Fix brakeman warnings

* Fix schema

* Fix Synth check
2025-03-03 12:47:30 -05:00
Zach Gollwitzer
e907b073ed Fix time period key conflicts (#1944) 2025-03-03 12:47:20 -05:00
Tony Vincent
4c4a4026c4 fix: Bug - Transcation Matching Dialog isn't Opening (#1942) 2025-03-03 11:34:03 -05:00
Zach Gollwitzer
c95bb082a9 Bump to v0.4.3
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-28 15:11:41 -05:00
Zach Gollwitzer
4d0df9b950 Escape quotations in CSV imports properly (#1929)
* Parse quotes in imports

* Update invalid CSV for test
2025-02-28 12:21:07 -05:00
Zach Gollwitzer
7c66f16750 Invert liability graphs to have correct signage (#1928) 2025-02-28 12:17:25 -05:00
Zach Gollwitzer
fa0248056d Show UI warning to user when they need provider data but have not setup Synth yet (#1926)
* Simplify provider concerns

* Update tests

* Add UI warning for missing Synth key if family requires external data
2025-02-28 11:35:10 -05:00
Tony Vincent
624faa10d0 fix: Don't show Billings on settings navbar when self-hosted (#1912)
* Do not show billing settings navbar item when self hosted

* Do not show billing settings navbar item when self hosted

* Add condition to settings helper

* Let Stripe::AuthenticationError bubble up
2025-02-28 09:35:00 -05:00
Zach Gollwitzer
9138bd2b76 Allow offline trade tickers (#1925) 2025-02-28 09:34:14 -05:00
Zach Gollwitzer
882857fcf0 Add transitions to buttons and other common design system elements (#1924) 2025-02-28 09:29:07 -05:00
Zach Gollwitzer
d6793dec05 Fix import configuration form so number format is applied (#1923)
* Fix number format form error when loading import

* Add test to verify import configuration was properly applied
2025-02-28 08:36:57 -05:00
Zach Gollwitzer
e771c8c1df Fix value wrapping on account balance in sidebar (#1922) 2025-02-28 08:35:14 -05:00
Zach Gollwitzer
58cc09f5ae Fix bad link in bug template
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-28 08:25:20 -05:00
Zach Gollwitzer
98c842d3b8 Add note about self hosted versions prior to opening bugs 2025-02-28 08:23:46 -05:00
Tony Vincent
fae781e1be Make tags scrollable again (#1921) 2025-02-28 07:53:05 -05:00
Tony Vincent
8208722247 Feat: Data "reset" button (#1913)
* feat: Allow admins to delete family data

* feat: Allow self-hosting users to delete cached data

* Remove system tests
2025-02-28 07:49:12 -05:00
Harshit Chaudhary
f7064fd4dd fixed example account balance (#1910) 2025-02-26 15:13:51 -05:00
Zach Gollwitzer
c610b0ba4b Dashboard design fixes (#1898)
* Dashboard design fixes

* Update dashboard greeting

* Remove sidebar toggle from settings breadcrumbs

* Autofocus and outlines for category dropdowns

* Lint fixes
2025-02-25 17:28:40 -05:00
Josh Pigford
a4874815a6 Add breadcrumbs support across application (#1897)
* Add breadcrumbs support across application

Fixes #1896

* Potential fix for tests

* Simplify breadcrumbs implementation

Remove complex breadcrumbs logic from controllers and concern, replacing with a simpler default approach that sets a basic breadcrumb based on the current controller name

* Refactor page header and breadcrumbs rendering

Remove complex breadcrumbs helper method and update layout to use more flexible content_for approach for page headers and breadcrumbs

* Add fallback breadcrumbs rendering to settings layout
2025-02-25 10:14:07 -06:00
Josh Pigford
763e222cdd Add Sentry user context to authentication concern 2025-02-25 08:48:26 -06:00
Josh Pigford
e8390a68d8 Reduce Sentry sampling rates for performance monitoring 2025-02-25 08:44:13 -06:00
Josh Pigford
0e76d753bd Replace StackProf with Vernier for performance profiling 2025-02-25 08:37:51 -06:00
Zach Gollwitzer
f5ff5332d5 Fix parent category sums in budget (#1894) 2025-02-24 12:51:13 -05:00
Zach Gollwitzer
0dea36ec7d Fix bulk edit drawer height 2025-02-24 12:48:03 -05:00
Syed Bariman Jan
95989a6c9b Add new category flow (#1857)
* resolve git issue

* Add new category flow

* Improve contrast checker

* make error message small

* update ui to match figma design

* realign color picker

* changes

* rename color picker controller to new category controller

* cleanup code

* cleanup code

* resize and realign icon avatar

* Fix js lint errors

Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com>

---------

Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com>
2025-02-24 11:08:05 -05:00
dependabot[bot]
ac9703031f Bump plaid from 36.0.0 to 36.1.0 (#1891)
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 36.0.0 to 36.1.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v36.0.0...v36.1.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 10:01:41 -05:00
dependabot[bot]
457e7062bf Bump selenium-webdriver from 4.28.0 to 4.29.1 (#1892)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.28.0 to 4.29.1.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/commits)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 10:01:05 -05:00
David Anyatonwu
32ef6ca154 Add exchange and currency fields to trade imports (#1822)
* Add exchange and currency fields to trade imports

* Add exchange_operating_mic support for trade imports - Added required columns and updated models

* refactor: remove exchange and currency columns

* fix: consolidate import schema and remove redundant columns

* feat: Enhance trade import with exchange_operating_mic support

* Revert changes to existing migration

* Simplify migration to use change method

* Restore previously deleted migration

* Remove unused import_col_labels method

* Update schema.rb after running migrations

* Update trade_import.rb and fix schema.rb with db:migrate:reset

* fix: improve trade import security creation

---------

Signed-off-by: David Anyatonwu <51977119+onyedikachi-david@users.noreply.github.com>
2025-02-24 10:00:24 -05:00
Zach Gollwitzer
fd95f8d2bd Drop linux/arm/v7 support
linux/arm/v7 is not compatible with the latest Tailwind package + takes a significant amount of time when using buildx.  Most Raspberry Pi OS now run on 64 bit ARM architecture, so we should still have significant coverage with linux/amd + linux/arm builds.

Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-21 17:12:06 -05:00
Zach Gollwitzer
da668f3dc0 Bump to v0.4.1
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-21 14:19:50 -05:00
Zach Gollwitzer
cc11fec08a Add git to Dockerfile for self hosted versioning info
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-21 13:40:16 -05:00
Josh Pigford
ce12e5b5c7 Remove inter-font from early access layout 2025-02-21 11:45:57 -06:00
Zach Gollwitzer
f96bb84c4c Bump to v0.4.0
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-21 12:39:49 -05:00
Zach Gollwitzer
d75be2282b New Design System + Codebase Refresh (#1823)
Since the very first 0.1.0-alpha.1 release, we've been moving quickly to add new features to the Maybe app. In doing so, some parts of the codebase have become outdated, unnecessary, or overly-complex as a natural result of this feature prioritization.

Now that "core" Maybe is complete, we're moving into a second phase of development where we'll be working hard to improve the accuracy of existing features and build additional features on top of "core". This PR is a quick overhaul of the existing codebase aimed to:

- Establish the brand new and simplified dashboard view (pictured above)
- Establish and move towards the conventions introduced in Cursor rules and project design overview #1788
- Consolidate layouts and improve the performance of layout queries
- Organize the core models of the Maybe domain (i.e. Account::Entry, Account::Transaction, etc.) and break out specific traits of each model into dedicated concerns for better readability
- Remove stale / dead code from codebase
- Remove overly complex code paths in favor of simpler ones
2025-02-21 11:57:59 -05:00
Zach Gollwitzer
8539ac7dec Fix import configuration failures (#1876) 2025-02-21 08:58:06 -05:00
Tony Vincent
c620d1fc1e fix: Ghost subcategories when parent category is deleted (#1874) 2025-02-20 09:03:30 -05:00
dependabot[bot]
79e1a2c0ff Bump logtail-rails from 0.2.9 to 0.2.10 (#1866)
Bumps [logtail-rails](https://github.com/logtail/logtail-ruby-rails) from 0.2.9 to 0.2.10.
- [Release notes](https://github.com/logtail/logtail-ruby-rails/releases)
- [Changelog](https://github.com/logtail/logtail-ruby-rails/blob/main/README.md)
- [Commits](https://github.com/logtail/logtail-ruby-rails/compare/v0.2.9...v0.2.10)

---
updated-dependencies:
- dependency-name: logtail-rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 14:43:19 -05:00
dependabot[bot]
e9b29f6726 Bump image_processing from 1.13.0 to 1.14.0 (#1865)
Bumps [image_processing](https://github.com/janko/image_processing) from 1.13.0 to 1.14.0.
- [Changelog](https://github.com/janko/image_processing/blob/master/CHANGELOG.md)
- [Commits](https://github.com/janko/image_processing/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: image_processing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:43:04 -05:00
dependabot[bot]
1b1e06070b Bump plaid from 35.1.0 to 36.0.0 (#1867)
Bumps [plaid](https://github.com/plaid/plaid-ruby) from 35.1.0 to 36.0.0.
- [Release notes](https://github.com/plaid/plaid-ruby/releases)
- [Changelog](https://github.com/plaid/plaid-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plaid/plaid-ruby/compare/v35.1.0...v36.0.0)

---
updated-dependencies:
- dependency-name: plaid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 11:42:42 -05:00
Tony Vincent
f35b70e936 fix: Transfers should always total to zero (#1859) 2025-02-17 11:42:30 -05:00
Zach Gollwitzer
8e339dcbe0 Dockerfile fixes
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-13 17:24:24 -05:00
Zach Gollwitzer
366862fee1 Fix bootsnap hanging in CI 2025-02-13 15:32:51 -05:00
Zach Gollwitzer
73f826fdf2 Split Dockerfile build commands for more clarity in CI 2025-02-13 15:04:15 -05:00
Zach Gollwitzer
999f1c5190 Fix font resolution paths 2025-02-13 14:58:11 -05:00
Zach Gollwitzer
849c58dd3e Maybe Design System Updates (#1856)
* Add geist font

* Design system css file

* Add cursor ui/ux rules

* Add shadows and shadow borders

* Replace primitives with tokens for common text and backgrounds

* Organize css

* Update switch and checkbox class names

* Add back global color variables
2025-02-13 11:31:07 -05:00
Zach Gollwitzer
c0e290a07e Add max time to Docker publish
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-12 15:07:24 -05:00
Josh Pigford
f1f2e103ce Enhance Plaid connection management with re-authentication and error handling (#1854)
* Enhance Plaid connection management with re-authentication and error handling

- Add support for Plaid item status tracking (good/requires_update)
- Implement re-authentication flow for Plaid connections
- Handle connection errors and provide user-friendly reconnection options
- Update Plaid link token generation to support item updates
- Add localization for new connection management states

* Remove redundant 'reconnect' localization for Plaid items
2025-02-12 12:59:35 -06:00
Zach Gollwitzer
08a2d35308 Fix off-center modals 2025-02-12 11:57:31 -05:00
Zach Gollwitzer
a82c0303ce Upgrade to Tailwind v4 (#1853)
* Upgrade to Tailwind v4

* Update max width classes

* Revert improper changes from Tailwind migration tool
2025-02-12 11:22:30 -05:00
Josh Pigford
945a39d035 Add manual entry option for two-factor authentication setup 2025-02-12 09:15:00 -06:00
Marvin M
d31d5c5467 fix: liabilities favorable direction is "down" (#1849) 2025-02-11 15:18:22 -05:00
Josh Pigford
ab7f6a56f0 Enhance securities de-duplication task 2025-02-11 11:11:14 -06:00
Josh Pigford
68d7cb5de6 Enhance security information retrieval and handling (#1826)
* Enhance security information retrieval and handling

- Add support for operating MIC codes in security info fetching
- Update security uniqueness validation to handle unknown securities
- Improve security creation and update logic in Plaid investment sync
- Update combobox and view components to handle operating MIC codes
- Add unknown flag for securities with incomplete information

* Update schema.rb

* Refactor the need for mic codes

* Don't fetch prices unless a security has the necessary mic code

* Deduplication

* Lint

* Update Securities and Plaid Investment Sync

- Modify PlaidInvestmentSync to return plaid_security for USD cash
- Add non-null constraint to Securities ticker column
- Update Securities fixture to use exchange_operating_mic instead of exchange_mic

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
2025-02-11 10:40:30 -06:00
Paul Imoke
fb6c6fa6bb fix: subcategories are not properly handled for budget allocations (#1844)
* fix: `allocated_spending` logic

* fix: subcategories exceeding parent limit

* refactor: budget allocations and max allocation logic

* feat: add stream for budget category form validation

* feat: update uncategorized value via stream, refactor confirm button with stream

* fix: ensure live updates for parent & sibling budgets in Turbo Stream

* fix: lint issues
2025-02-11 09:28:06 -05:00
Daniel Esteves
077694bbde feat(import): add currency and number format support for CSV imports (#1819)
* feat(import): add currency and number format support for CSV imports

* feat(import): add currency and format for mint and trade

* fix(imports): remove currency field in favor of currency csv col

* fix(imports): remove validate column from import model

* test(import): add some tests for imports

* test(import): add some tests for generate_rows_from_csv

* fix: change method name for import model

* fix: change before validation

---------

Co-authored-by: danestves <danestves@users.noreply.github.com>
2025-02-10 15:31:28 -05:00
dependabot[bot]
9e5f1574bc Bump sentry-rails from 5.22.3 to 5.22.4 (#1840)
Bumps [sentry-rails](https://github.com/getsentry/sentry-ruby) from 5.22.3 to 5.22.4.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-ruby/compare/5.22.3...5.22.4)

---
updated-dependencies:
- dependency-name: sentry-rails
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 12:28:53 -05:00
dependabot[bot]
26197eda85 Bump webmock from 3.24.0 to 3.25.0 (#1839)
Bumps [webmock](https://github.com/bblimke/webmock) from 3.24.0 to 3.25.0.
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.24.0...v3.25.0)

---
updated-dependencies:
- dependency-name: webmock
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 12:22:33 -05:00
dependabot[bot]
1dbf1ad74e Bump sentry-ruby from 5.22.3 to 5.22.4 (#1841)
Bumps [sentry-ruby](https://github.com/getsentry/sentry-ruby) from 5.22.3 to 5.22.4.
- [Release notes](https://github.com/getsentry/sentry-ruby/releases)
- [Changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-ruby/compare/5.22.3...5.22.4)

---
updated-dependencies:
- dependency-name: sentry-ruby
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 12:22:20 -05:00
dependabot[bot]
ab632cf0be Bump ruby-lsp-rails from 0.3.31 to 0.4.0 (#1842)
Bumps [ruby-lsp-rails](https://github.com/Shopify/ruby-lsp-rails) from 0.3.31 to 0.4.0.
- [Release notes](https://github.com/Shopify/ruby-lsp-rails/releases)
- [Commits](https://github.com/Shopify/ruby-lsp-rails/compare/v0.3.31...v0.4.0)

---
updated-dependencies:
- dependency-name: ruby-lsp-rails
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 12:22:10 -05:00
dependabot[bot]
2b83fc787f Bump good_job from 4.8.2 to 4.9.0 (#1843)
Bumps [good_job](https://github.com/bensheldon/good_job) from 4.8.2 to 4.9.0.
- [Release notes](https://github.com/bensheldon/good_job/releases)
- [Changelog](https://github.com/bensheldon/good_job/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bensheldon/good_job/compare/v4.8.2...v4.9.0)

---
updated-dependencies:
- dependency-name: good_job
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:32:44 -05:00
Zach Gollwitzer
536c82f2aa Feature: Add the ability to "revert" a CSV import (#1814)
* Allow reverting imports

* Fix tests

* Add currency column to all imports

* Don't auto-enrich demo account
2025-02-07 15:36:05 -05:00
Josh Pigford
60925bd16c Remove redundant amount validation from Account Holding model 2025-02-07 10:58:41 -06:00
Josh Pigford
cf23673003 Fix Account Holding validation and synchronization (#1818)
* Fix Account Holding validation and synchronization

Fixes #1781

- Add comprehensive validations for Account::Holding
- Implement validation to ensure amount matches qty * price
- Update Account::Syncer to include qty and price during synchronization
- Add database constraints for holding attributes and calculations

* Remove database check constraints for Account Holdings

Align with project convention of keeping complex validations in ActiveRecord
- Remove database-level check constraints for quantity, price, and amount
- Maintain database-level null and unique constraints
- Prepare for more flexible validation in the model layer
2025-02-07 10:42:01 -06:00
Zach Gollwitzer
0dc25cda22 Plaid EU webhooks migration task (#1825)
* Add data migration for EU Plaid webhooks

* Fix task
2025-02-07 11:09:38 -05:00
Zach Gollwitzer
5eb5ec7aef fix: Plaid webhook verification (#1824)
* Fix Plaid webhook verification

* Fix client creation in webhook controller
2025-02-07 10:35:42 -05:00
Zach Gollwitzer
331de2f997 Capture webhook errors in Sentry 2025-02-07 09:54:15 -05:00
Josh Pigford
972c850d27 MFA qr code fix 2025-02-06 15:26:50 -06:00
Josh Pigford
78e34d68e9 fix: Format budget estimated income and spending with two decimal places 2025-02-06 14:40:58 -06:00
Josh Pigford
21dc4b80f3 fix: Round average monthly budget to two decimal places 2025-02-06 14:35:42 -06:00
Josh Pigford
842e37658c Multi-factor authentication (#1817)
* Initial pass

* Tests for MFA and locale cleanup

* Brakeman

* Update two-factor authentication status styling

* Update app/models/user.rb

Co-authored-by: Zach Gollwitzer <zach@maybe.co>
Signed-off-by: Josh Pigford <josh@joshpigford.com>

* Refactor MFA verification and session handling in tests

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
Co-authored-by: Zach Gollwitzer <zach@maybe.co>
2025-02-06 14:16:53 -06:00
Josh Pigford
7ba9063e04 feat: Add institution details to Plaid items (#1816)
* feat: Add institution details to Plaid items

- Fetch and store institution URL, ID, and primary color for Plaid items
- Update PlaidItem model to retrieve and save institution metadata
- Add new method in Plaid provider to get institution details
- Update account logo view to use institution domain for logo generation

* Add institution domain method to Account model

- Extract institution domain logic from view to Account model
- Simplify logo view by using new institution_domain method
- Improve code reusability and separation of concerns
2025-02-06 08:57:24 -06:00
Marvin M
df5f4c83fe fix: Save completely allocated budget (#1811)
Signed-off-by: Marvin M <39344769+M123-dev@users.noreply.github.com>
2025-02-05 13:20:19 -05:00
Josh Pigford
e3ef1dd6b4 Increase profile image size limit 2025-02-05 12:04:24 -06:00
Josh Pigford
4aba9d1c0b Add scope to filter transactions from active accounts (#1810)
* Add scope to filter transactions from active accounts

* Add test for transfer match candidates with active accounts

* Refactor active account filtering for transactions and entries
2025-02-05 11:52:44 -06:00
Josh Pigford
cf014bc24f Reduce Sentry sample rate 2025-02-05 11:33:59 -06:00
Josh Pigford
90f1ff8a0b Increase limit on ticker search 2025-02-05 11:02:12 -06:00
Josh Pigford
b84a33c09d Refactor transaction enrichment to support batch processing (#1803)
* Refactor transaction enrichment to support batch processing

- Add method to enrich transactions in batches
- Implement job scheduling for unenriched transactions
- Improve logging and error handling for transaction enrichment

* Re-enable enrichment

* Fix transaction enrichment query to use correct table references

- Update queries to explicitly join and reference account_entries and account_transactions tables
- Remove unnecessary name presence check before enrichment
- Improve query precision for unenriched transaction selection

* Optimize transaction enrichment query joins

- Refactor database joins to use explicit table references
- Improve query performance for unenriched transaction selection
- Ensure correct table aliasing in enrichment methods

* Remove deprecated data enrichment job and method

- Delete EnrichDataJob as it's no longer used
- Remove `enrich_data_later` method from Account model
- Update Account::Syncer to directly call `enrich_data` instead of scheduling a job
2025-02-05 10:34:28 -06:00
Zach Gollwitzer
abd932c894 Update issue templates 2025-02-05 10:57:33 -05:00
Zach Gollwitzer
5b083c9e33 Fix budget allocation forms from resetting and clearing data on slow networks (#1804)
* First pass

* Fix null constraint bug for budget category assignment

* Fix autofocus reset when allocating budget

* Lint fix
2025-02-05 09:09:38 -05:00
Josh Pigford
f498212b2d Add migration generation guideline to project conventions 2025-02-04 20:25:19 -06:00
Saleh Almatrafi
37aab45c19 Fix incorrect currency assignment for stock prices #1623 (#1798) 2025-02-04 14:27:00 -05:00
Zach Gollwitzer
c9c5eb315a Split family and Plaid item syncs into multiple jobs (#1799)
* Split family and Plaid item syncs into multiple jobs

* fix test expectations
2025-02-04 14:22:44 -05:00
Zach Gollwitzer
75c8627577 Temporarily disable data enrichment jobs until optimized 2025-02-04 13:54:14 -05:00
Zach Gollwitzer
15e8281d46 Remove thread allocation from catch-all queue 2025-02-04 13:39:52 -05:00
Zach Gollwitzer
283d9cd8e2 Fix GoodJob config syntax 2025-02-04 13:29:27 -05:00
Zach Gollwitzer
3e06017ae1 Use correct config scope for GoodJob
Signed-off-by: Zach Gollwitzer <zach@maybe.co>
2025-02-04 13:21:10 -05:00
Josh Pigford
058830591f Increase database connection pool size 2025-02-04 12:07:46 -06:00
Josh Pigford
32d826c047 Increase good_job threads 2025-02-04 12:06:05 -06:00
Josh Pigford
bdec61f312 Improve HoldingCalculator Security Preloading
- Include securities from both trades and current holdings
- Ensure unique securities are preloaded
- Enhance logging for preloaded securities
2025-02-04 11:02:50 -06:00
Zach Gollwitzer
9c846e7de4 Fix crypto.randomUUID errors when adding holdings (#1795) 2025-02-04 11:35:50 -05:00
Josh Pigford
4c158934d0 Improve Security Price Loading with Robust Error Handling
- Add error handling for individual security price loading
- Log detailed error information for problematic securities
- Prevent single security errors from halting entire price loading process
- Enhance logging with more specific security identification details
2025-02-03 19:57:58 -06:00
Josh Pigford
50e5ffb257 Enhance HoldingCalculator with Logging and Error Handling 2025-02-03 19:54:32 -06:00
Josh Pigford
983729cbdf Logtail 2025-02-03 19:49:41 -06:00
Zach Gollwitzer
89027f1fbf EU only allows transactions Plaid product 2025-02-03 20:39:08 -05:00
Zach Gollwitzer
2a338eb01b Cursor rules and project design overview (#1788)
* Add cursor rules directory

* Complete project design rules
2025-02-03 19:30:03 -05:00
553 changed files with 10031 additions and 6769 deletions

View File

@@ -1,64 +0,0 @@
<!-- Copy this file to .cursorrules in the root of the project on your local machine if you'd like to use these rules with Cursor. -->
You are an expert in Ruby, Ruby on Rails, Postgres, Tailwind, Stimulus, Hotwire and Turbo and always use the latest stable versions of those technologies.
**Code Style and Structure**
- Write concise, technical Ruby code with accurate examples.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., is_loading, has_error).
- Structure files: models, controllers, views, helpers, services, jobs, mailers.
**Naming Conventions**
- Use snake_case for file names and directories (e.g., app/models/user_profile.rb).
- Use CamelCase for classes and modules (e.g., UserProfile).
**Ruby on Rails Usage**
- Use Rails conventions for MVC structure.
- Favor scopes over class methods for queries.
- Use strong parameters for mass assignment protection.
- Use partials to DRY up views.
**Syntax and Formatting**
- Use two spaces for indentation.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use descriptive method names and keep methods short.
**Commenting Code**
- Write clear, concise comments to explain the purpose of individual functions and methods.
- Use comments to describe the intent and functionality of complex logic.
- Avoid redundant comments that state the obvious.
**UI and Styling**
- Use Tailwind CSS for styling.
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
- Use Stimulus for JavaScript behavior.
- Use Turbo for asynchronous actions and updates.
**Performance Optimization**
- Use eager loading to avoid N+1 queries.
- Cache expensive queries and partials where appropriate.
- Use background jobs for long-running tasks.
- Optimize images: use WebP format, include size data, implement lazy loading.
**Database Querying & Data Model Creation**
- Use ActiveRecord for data querying and model creation.
- Favor database constraints and indexes for data integrity and performance.
- Use migrations to manage schema changes.
**Key Conventions**
- Follow Rails best practices for RESTful routing.
- Optimize for performance and security.
- Use environment variables for configuration.
- Write tests for models, controllers, and features.
**AI Guidelines**
- Follow the users requirements carefully & to the letter.
- Confirm, then write code!
- Suggest solutions that I didn't think about—anticipate my needs
- Focus on readability over being performant.
- Fully implement all requested functionality.
- Leave NO todos, placeholders or missing pieces.
- Don't say things like "additional logic can be added here" — instead, add the logic.
- Be concise. Minimize any other prose.
- Consider new technologies and contrarian ideas, not just the conventional wisdom
- If I ask for adjustments to code, do not repeat all of my code unnecessarily. Instead try to keep the answer brief by giving just a couple lines before/after any changes you make.

View File

@@ -0,0 +1,84 @@
---
description: This rule explains the project's tech stack and code conventions
globs: *
---
This rule serves as high-level documentation for how the Maybe codebase is structured.
## Rules for AI
- Use this file to understand how the codebase works
- Treat this rule/file as your "source of truth" when making code recommendations
- When creating migrations, always use `rails g migration` instead of creating the file yourself
## Project Tech Stack
- Web framework: Ruby on Rails
- Minitest + fixtures for testing
- Propshaft for asset pipeline
- Hotwire Turbo/Stimulus for SPA-like UI/UX
- TailwindCSS for styles
- Lucide Icons for icons
- Database: PostgreSQL
- Jobs: GoodJob
- External
- Payments: Stripe
- User bank data syncing: Plaid
- Market data: Synth (our custom API)
## Project conventions
These conventions should be used when writing code for Maybe.
### Convention 1: Minimize dependencies, vanilla Rails is plenty
Dependencies are a natural part of building software, but we aim to minimize them when possible to keep this open-source codebase easy to understand, maintain, and contribute to.
- Push Rails to its limits before adding new dependencies
- When a new dependency is added, there must be a strong technical or business reason to add it
- When adding dependencies, you should favor old and reliable over new and flashy
### Convention 2: Leverage POROs and concerns over "service objects"
This codebase adopts a "skinny controller, fat models" convention. Furthermore, we put almost _everything_ directly in the `app/models/` folder and avoid separate folders for business logic such as `app/services/`.
- Organize large pieces of business logic into Rails concerns and POROs (Plain ole' Ruby Objects)
- While a Rails concern _may_ offer shared functionality (i.e. "duck types"), it can also be a "one-off" concern that is only included in one place for better organization and readability.
- When concerns are used for code organization, they should be organized around the "traits" of a model; not for simply moving code to another spot in the codebase.
- When possible, models should answer questions about themselves—for example, we might have a method, `account.balance_series` that returns a time-series of the account's most recent balances. We prefer this over something more service-like such as `AccountSeries.new(account).call`.
### Convention 3: Prefer server-side solutions over client-side solutions
- When possible, leverage Turbo frames over complex, JS-driven client-side solutions
- When writing a client-side solution, use Stimulus controllers and keep it simple!
- Especially when dealing with money and currencies, calculate + format server-side and then pass that to the client to display
- Keep client-side code for where it truly shines. For example, [bulk_select_controller.js](mdc:app/javascript/controllers/bulk_select_controller.js) is a case where server-side solutions would degrade the user experience significantly. When bulk-selecting entries, client-side solutions are the way to go and Stimulus provides the right toolset to achieve this.
### Convention 4: Sacrifice performance, optimize for simplicitly and clarity
This codebase is still young. We are still rapidly iterating on domain designs and features. Because of this, code should be optimized for simplicitly and clarity over performance.
- Focus on good OOP design first, performance second
- Be mindful of large performance bottlenecks, but don't sweat the small stuff
### Convention 5: Prefer semantic, native HTML features
The HTML spec has improved tremendously over the years and offers a ton of functionality out of the box. We prefer semantic, native HTML solutions over JS-based ones. A few examples of this include:
- Using the `dialog` element for modals
- Using `summary` / `details` elements for disclosures (or `popover` attribute)
The Hotwire suite (Turbo/Stimulus) works very well with these native elements and we optimize for this.
### Convention 6: Use Minitest + Fixtures for testing, minimize fixtures
Due to the open-source nature of this project, we have chosen Minitest + Fixtures for testing to maximize familiarity and predictability.
- Always use Minitest and fixtures for testing.
- Keep fixtures to a minimum. Most models should have 2-3 fixtures maximum that represent the "base cases" for that model. "Edge cases" should be created on the fly, within the context of the test which it is needed.
- For tests that require a large number of fixture records to be created, use Rails helpers such as [entries_test_helper.rb](mdc:test/support/account/entries_test_helper.rb) to act as a "factory" for creating these. For a great example of this, check out [balance_calculator_test.rb](mdc:test/models/account/balance_calculator_test.rb)
### Convention 7: Use ActiveRecord for complex validations, DB for simple ones, keep business logic out of DB
- Enforce `null` checks, unique indexes, and other simple validations in the DB
- ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible.
- Complex validations and business logic should remain in ActiveRecord

View File

@@ -0,0 +1,134 @@
---
description: This rule explains the system architecture and data flow of the Rails app
globs: *
---
This file outlines how the codebase is structured and how data flows through the app.
This is a personal finance application built in Ruby on Rails. The primary domain entities for this app are outlined below. For an authoritative overview of the relationships, [schema.rb](mdc:db/schema.rb) is the source of truth.
## App Modes
The Maybe app runs in two distinct "modes", dictated by `Rails.application.config.app_mode`, which can be `managed` or `self_hosted`.
- "Managed" - in managed mode, the Maybe team operates and manages servers for users
- "Self Hosted" - in self hosted mode, users host the Maybe app on their own infrastructure, typically through Docker Compose. We have an example [docker-compose.example.yml](mdc:docker-compose.example.yml) file that runs [Dockerfile](mdc:Dockerfile) for this mode.
## Families and Users
- `Family` - all Stripe subscriptions, financial accounts, and the majority of preferences are stored at the [family.rb](mdc:app/models/family.rb) level.
- `User` - all [session.rb](mdc:app/models/session.rb) happen at the [user.rb](mdc:app/models/user.rb) level. A user belongs to a `Family` and can either be an `admin` or a `member`. Typically, a `Family` has a single admin, or "head of household" that manages finances while there will be several `member` users who can see the family's finances from varying perspectives.
## Currency Preference
Each `Family` selects a currency preference. This becomes the "main" currency in which all records are "normalized" to via [exchange_rate.rb](mdc:app/models/exchange_rate.rb) records so that the Maybe app can calculate metrics, historical graphs, and other insights in a single family currency.
## Accounts
The center of the app's domain is the [account.rb](mdc:app/models/account.rb). This represents a single financial account that has a `balance` and `currency`. For example, an `Account` could be "Chase Checking", which is a single financial account at Chase Bank. A user could have multiple accounts at a single institution (i.e. "Chase Checking", "Chase Credit Card", "Chase Savings") or an account could be a standalone account, such as "My Home" (a primary residence).
### Accountables
In the app, [account.rb](mdc:app/models/account.rb) is a Rails "delegated type" with the following subtypes (separate DB tables). Each account has a `classification` or either `asset` or `liability`. While the types are a flat hierarchy, below, they have been organized by their classification:
- Asset accountables
- [depository.rb](mdc:app/models/depository.rb) - a typical "bank account" such as a savings or checking account
- [investment.rb](mdc:app/models/investment.rb) - an account that has "holdings" such as a brokerage, 401k, etc.
- [crypto.rb](mdc:app/models/crypto.rb) - an account that tracks the value of one or more crypto holdings
- [property.rb](mdc:app/models/property.rb) - an account that tracks the value of a physical property such as a house or rental property
- [vehicle.rb](mdc:app/models/vehicle.rb) - an account that tracks the value of a vehicle
- [other_asset.rb](mdc:app/models/other_asset.rb) - an asset that cannot be classified by the other account types. For example, "jewelry".
- Liability accountables
- [credit_card.rb](mdc:app/models/credit_card.rb) - an account that tracks the debt owed on a credit card
- [loan.rb](mdc:app/models/loan.rb) - an account that tracks the debt owed on a loan (i.e. mortgage, student loan)
- [other_liability.rb](mdc:app/models/other_liability.rb) - a liability that cannot be classified by the other account types. For example, "IOU to a friend"
### Account Balances
An account [balance.rb](mdc:app/models/account/balance.rb) represents a single balance value for an account on a specific `date`. A series of balance records is generated daily for each account and is how we show a user's historical balance graph.
- For simple accounts like a "Checking Account", the balance represents the amount of cash in the account for a date.
- For a more complex account like "Investment Brokerage", the `balance` represents the combination of the "cash balance" + "holdings value". Each accountable type has different components that make up the "balance", but in all cases, the "balance" represents "How much the account is worth" (when `classification` is `asset`) or "How much is owed on the account" (when `classification` is `liability`)
All balances are calculated daily by [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb).
### Account Holdings
An account [holding.rb](mdc:app/models/account/holding.rb) applies to [investment.rb](mdc:app/models/investment.rb) type accounts and represents a `qty` of a certain [security.rb](mdc:app/models/security.rb) at a specific `price` on a specific `date`.
For investment accounts with holdings, [holding_calculator.rb](mdc:app/models/account/holding_calculator.rb) is used to calculate the daily historical holding quantities and prices, which are then rolled up into a final "Balance" for the account in [balance_calculator.rb](mdc:app/models/account/balance_calculator.rb).
### Account Entries
An account [entry.rb](mdc:app/models/account/entry.rb) is also a Rails "delegated type". `Account::Entry` represents any record that _modifies_ an `Account` [balance.rb](mdc:app/models/account/balance.rb) and/or [holding.rb](mdc:app/models/account/holding.rb). Therefore, every entry must have a `date`, `amount`, and `currency`.
The `amount` of an [entry.rb](mdc:app/models/account/entry.rb) is a signed value. A _negative_ amount is an "inflow" of money to that account. A _positive_ value is an "outflow" of money from that account. For example:
- A negative amount for a credit card account represents a "payment" to that account, which _reduces_ its balance (since it is a `liability`)
- A negative amount for a checking account represents an "income" to that account, which _increases_ its balance (since it is an `asset`)
- A negative amount for an investment/brokerage trade represents a "sell" transaction, which _increases_ the cash balance of the account
There are 3 entry types, defined as [entryable.rb](mdc:app/models/account/entryable.rb) records:
- `Account::Valuation` - an account [valuation.rb](mdc:app/models/account/valuation.rb) is an entry that says, "here is the value of this account on this date". It is an absolute measure of an account value / debt. If there is an `Account::Valuation` of 5,000 for today's date, that means that the account balance will be 5,000 today.
- `Account::Transaction` - an account [transaction.rb](mdc:app/models/account/transaction.rb) is an entry that alters the account balance by the `amount`. This is the most common type of entry and can be thought of as an "income" or "expense".
- `Account::Trade` - an account [trade.rb](mdc:app/models/account/trade.rb) is an entry that only applies to an investment account. This represents a "buy" or "sell" of a holding and has a `qty` and `price`.
### Account Transfers
A [transfer.rb](mdc:app/models/transfer.rb) represents a movement of money between two accounts. A transfer has an inflow [transaction.rb](mdc:app/models/account/transaction.rb) and an outflow [transaction.rb](mdc:app/models/account/transaction.rb). The Maybe system auto-matches transfers based on the following criteria:
- Must be from different accounts
- Must be within 4 days of each other
- Must be the same currency
- Must be opposite values
There are two primary forms of a transfer:
- Regular transfer - a normal movement of money between two accounts. For example, "Transfer $500 from Checking account to Brokerage account".
- Debt payment - a special form of transfer where the _receiver_ of funds is a [loan.rb](mdc:app/models/loan.rb) type account.
Regular transfers are typically _excluded_ from income and expense calculations while a debt payment is considered an "expense".
## Plaid Items
A [plaid_item.rb](mdc:app/models/plaid_item.rb) represents a "connection" maintained by our external data provider, Plaid in the "hosted" mode of the app. An "Item" has 1 or more [plaid_account.rb](mdc:app/models/plaid_account.rb) records, which are each associated 1:1 with an internal Maybe [account.rb](mdc:app/models/account.rb).
All relevant metadata about the item and its underlying accounts are stored on [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb), while the "normalized" data is then stored on internal Maybe domain models.
## "Syncs"
The Maybe app has the concept of a [syncable.rb](mdc:app/models/concerns/syncable.rb), which represents any model which can have its data "synced" in the background. "Syncables" include:
- `Account` - an account "sync" will sync account holdings, balances, and enhance transaction metadata
- `PlaidItem` - a Plaid Item "sync" fetches data from Plaid APIs, normalizes that data, stores it on internal Maybe models, and then finally performs an "Account sync" for each of the underlying accounts created from the Plaid Item.
- `Family` - a Family "sync" loops through the family's Plaid Items and individual Accounts and "syncs" each of them. A family is synced once per day, automatically through [auto_sync.rb](mdc:app/controllers/concerns/auto_sync.rb).
Each "sync" creates a [sync.rb](mdc:app/models/sync.rb) record in the database, which keeps track of the status of the sync, any errors that it encounters, and acts as an "audit table" for synced data.
Below are brief descriptions of each type of sync in more detail.
### Account Syncs
The most important type of sync is the account sync. It is orchestrated by the account [syncer.rb](mdc:app/models/account/syncer.rb), and performs a few important tasks:
- Auto-matches transfer records for the account
- Calculates holdings and balances for the account
- Enriches transaction data
- Converts account balances that are not in the family's preferred currency to the preferred currency
An account sync happens every time an [entry.rb](mdc:app/models/account/entry.rb) is updated.
### Plaid Item Syncs
A Plaid Item sync is an ETL (extract, transform, load) operation:
1. [plaid_item.rb](mdc:app/models/plaid_item.rb) fetches data from the external Plaid API
2. [plaid_item.rb](mdc:app/models/plaid_item.rb) creates and loads this data to [plaid_account.rb](mdc:app/models/plaid_account.rb) records
3. [plaid_item.rb](mdc:app/models/plaid_item.rb) and [plaid_account.rb](mdc:app/models/plaid_account.rb) transform and load data to [account.rb](mdc:app/models/account.rb) and [entry.rb](mdc:app/models/account/entry.rb), the internal Maybe representations of the data.
### Family Syncs
A family sync happens once daily via [auto_sync.rb](mdc:app/controllers/concerns/auto_sync.rb). A family sync is an "orchestrator" of Account and Plaid Item syncs.

View File

@@ -0,0 +1,13 @@
---
description: This file describes Maybe's design system and how views should be styled
globs: app/views/**,app/helpers/**,app/javascript/controllers/**
---
Use this rule whenever you are writing html, css, or even styles in Stimulus controllers that use D3.js.
The codebase uses TailwindCSS v4.x (the newest version) with a custom design system defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css)
- Always start by referencing [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) to see the base primitives and tokens we use in the codebase
- Always generate semantic HTML
- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so
- Always favor the "utility first" Tailwind approach. Reusable style classes should not be created often. Code should be reused primarily through ERB partials.
- Always prefer using the utility "tokens" defined in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) when possible. For example, use `text-primary` rather than `text-gray-900`.

View File

@@ -1,31 +1,61 @@
---
name: Bug report
about: Create a report to help us improve
title: 'Bug: '
about: Open a bug report when you experience broken functionality within the latest
version of the Maybe app
title: 'Bug: [Add descriptive title here]'
labels: ''
assignees: ''
---
**Where did this bug occur? (required)**
## Before you start (required)
- [ ] I am a self-hosted user reporting a bug from my self hosted app
- [ ] I am a user of Maybe's paid app
### General checklist
_Please note, if you are reporting a bug with sensitive data, please open an Intercom chat from within the app for help_
- [ ] I have removed personal / sensitive data from screenshots and logs
- [ ] I have searched [existing issues](https://github.com/maybe-finance/maybe/issues?q=is:issue) and [discussions](https://github.com/maybe-finance/maybe/discussions) to ensure this is not a duplicate issue
### How are you using Maybe?
- [ ] I am a paying Maybe customer (hosted version)
- Paying Maybe users can also open requests in Intercom (if there is sensitive info involved)
- [ ] I am a self-hosted user
### Self hoster checklist
_Paying, hosted users should delete this entire section._
If you are a self-hosted user, please complete all of the information below. Issues with incomplete information will be marked as `Needs Info` to help our small team prioritize bug fixes.
- Self hosted app commit SHA (find in user menu): [enter commit sha here]
- [ ] I have confirmed that my app's commit is the latest version of Maybe
- Where are you hosting?
- [ ] Render
- [ ] Docker Compose
- [ ] Umbrel
- [ ] Other (please specify)
---
## Bug description
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
### To Reproduce
Be as specific as possible so Maybe maintainers can quickly reproduce the bug you're experiencing.
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
### Expected behavior
**Screenshots / Recordings**
If applicable, add screenshots or short video recordings to help show the bug in more detail.
What is the intended behavior that you would expect?
### Screenshots and/or recordings
We highly recommend providing additional context with screenshots and/or screen recordings. This will _significantly_ improve the chances of the bug being addressed and fixed quickly.

View File

@@ -7,15 +7,36 @@ assignees: ''
---
**PLEASE READ before opening an issue:**
## Before you start (required)
- Is this a feature request? Please [open a feature request discussion](https://github.com/maybe-finance/maybe/discussions/new?category=feature-requests).
- Do you need help or have a question? Please [open a discussion](https://github.com/maybe-finance/maybe/discussions/new/choose) or [join our Discord](https://link.maybe.co/discord) and post to the "help" channel.
### Is this a bug?
----------------------
A bug is _broken functionality_ of the app (i.e. it prevents you from using the app). For bugs, please use the ["Bug Report" template](https://github.com/maybe-finance/maybe/issues) instead.
**Is this issue related to a problem? Please describe.**
### Is this a bug with _sensitive info_?
**Describe the work that needs to be done to address this issue**
If you are a _paying_ Maybe user, you can open a support request in Intercom.
**Additional context**
### Is this a feature request?
A feature request is functionality that you would like that is not already on our [Roadmap](https://github.com/maybe-finance/maybe/wiki/Roadmap).
All feature requests should be opened as Discussions here:
https://github.com/maybe-finance/maybe/discussions/categories/feature-requests
Be sure to search existing discussions prior to opening a new feature request.
### Is this related to Docker and/or hosting for self hosting?
If you are having a Docker configuration issue, please do not open a Github issue unless you've identified a bug in our Dockerfile. To get help with self hosting, there are several options:
- **First**: Read our [self hosting guides](https://github.com/maybe-finance/maybe/tree/main/docs/hosting) and follow them step-by-step
- Open a [General Discussion](https://github.com/maybe-finance/maybe/discussions/categories/general)
- Make a post in the "Self hosted" channel in our [Discord](https://link.maybe.co/discord)
---
## Issue description
If your issue does not fall into the categories above, please provide a **descriptive and complete** overview of your issue.

View File

@@ -1,6 +1,13 @@
name: Publish Docker image
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref (tag or commit SHA) to build'
required: true
type: string
default: 'main'
push:
tags:
- 'v*'
@@ -22,6 +29,8 @@ jobs:
name: Build docker image
needs: [ ci ]
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
@@ -31,6 +40,8 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -65,9 +76,10 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64,linux/arm/v7' || 'linux/amd64,linux/arm64' }}
platforms: 'linux/amd64,linux/arm64'
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=A multi-arch Docker image for the Maybe Rails app
build-args: BUILD_COMMIT_SHA=${{ github.sha }}

View File

@@ -4,6 +4,8 @@ It means so much that you're interested in contributing to Maybe! Seriously. Tha
## House Rules
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
- While totally optional, consider using Cursor + VSCode as it will automatically apply our project conventions to your code via the `.cursor/rules` directory.
- Before contributing, please check if it already exists in [issues](https://github.com/maybe-finance/maybe/issues) or [PRs](https://github.com/maybe-finance/maybe/pulls)
- Given the speed at which we're moving on the codebase, we don't assign issues or "give" issues to anyone.
- When multiple PRs are submitted for the same issue, we take the one that most succinctly & efficiently solves a given problem and stays within the scope of work.

View File

@@ -12,34 +12,36 @@ RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libvips postgresql-client
# Set production environment
ARG BUILD_COMMIT_SHA
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
BUNDLE_WITHOUT="development" \
BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA}
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get install --no-install-recommends -y build-essential git libpq-dev pkg-config
RUN apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config
# Install application gems
COPY .ruby-version Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
RUN bundle install
RUN rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git
RUN bundle exec bootsnap precompile --gemfile -j 0
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
RUN bundle exec bootsnap precompile -j 0 app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base

10
Gemfile
View File

@@ -21,16 +21,19 @@ gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
gem "stimulus-rails"
gem "turbo-rails"
gem "hotwire_combobox"
# Temporary pin to commit to fix crypto.randomUUID() errors. Revert this when the change has been released.
gem "hotwire_combobox", github: "josefarias/hotwire_combobox", ref: "b827048a8305e1115d5f96931ba1c9750d1e59fc"
# Background Jobs
gem "good_job"
# Error logging
gem "stackprof"
gem "vernier"
gem "rack-mini-profiler"
gem "sentry-ruby"
gem "sentry-rails"
gem "logtail-rails"
# Active Storage
gem "aws-sdk-s3", "~> 1.177.0", require: false
@@ -52,6 +55,9 @@ gem "redcarpet"
gem "stripe"
gem "intercom-rails"
gem "plaid"
gem "rotp", "~> 6.3"
gem "rqrcode", "~> 2.2"
gem "ruby-openai"
group :development, :test do
gem "debug", platforms: %i[mri windows]

View File

@@ -1,3 +1,14 @@
GIT
remote: https://github.com/josefarias/hotwire_combobox.git
revision: b827048a8305e1115d5f96931ba1c9750d1e59fc
ref: b827048a8305e1115d5f96931ba1c9750d1e59fc
specs:
hotwire_combobox (0.3.2)
platform_agent (>= 1.0.1)
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
GIT
remote: https://github.com/maybe-finance/lucide-rails.git
revision: 272e5fb8418ea458da3995d6abe0ba0ceee9c9f0
@@ -127,6 +138,7 @@ GEM
xpath (~> 3.2)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
climate_control (1.2.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
@@ -155,6 +167,7 @@ GEM
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
event_stream_parser (1.0.0)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (2.12.2)
@@ -180,7 +193,7 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
good_job (4.8.2)
good_job (4.9.3)
activejob (>= 6.1.0)
activerecord (>= 6.1.0)
concurrent-ruby (>= 1.3.1)
@@ -194,13 +207,9 @@ GEM
actioncable (>= 7.0.0)
listen (>= 3.0.0)
railties (>= 7.0.0)
hotwire_combobox (0.3.2)
rails (>= 7.0.7.2)
stimulus-rails (>= 1.2)
turbo-rails (>= 1.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14)
i18n-tasks (1.0.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@@ -209,9 +218,10 @@ GEM
parser (>= 3.2.2.1)
rails-i18n
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.1.0)
actionpack (>= 6.0.0)
@@ -229,7 +239,7 @@ GEM
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.9.1)
json (2.10.1)
jwt (2.10.1)
base64
language_server-protocol (3.17.0.4)
@@ -239,10 +249,22 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.5)
logger (1.6.6)
logtail (0.1.15)
msgpack (~> 1.0)
logtail-rack (0.2.6)
logtail (~> 0.1)
rack (>= 1.2, < 4.0)
logtail-rails (0.2.10)
actionpack (>= 5.0.0)
activerecord (>= 5.0.0)
logtail (~> 0.1, >= 0.1.14)
logtail-rack (~> 0.1)
railties (>= 5.0.0)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -253,12 +275,14 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
mini_magick (4.13.2)
mini_magick (5.1.2)
benchmark
logger
mini_mime (1.1.5)
minitest (5.25.4)
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.7.5)
msgpack (1.8.0)
multipart-post (2.4.1)
net-http (0.6.0)
uri
@@ -272,34 +296,37 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.2-aarch64-linux-gnu)
nokogiri (1.18.3-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-aarch64-linux-musl)
nokogiri (1.18.3-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm-linux-gnu)
nokogiri (1.18.3-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-arm-linux-musl)
nokogiri (1.18.3-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.2-arm64-darwin)
nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-darwin)
nokogiri (1.18.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-gnu)
nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.2-x86_64-linux-musl)
nokogiri (1.18.3-x86_64-linux-musl)
racc (~> 1.4)
octokit (9.2.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
pagy (9.3.3)
parallel (1.26.3)
parser (3.3.7.0)
parser (3.3.7.1)
ast (~> 2.4.1)
racc
pg (1.5.9)
plaid (35.1.0)
plaid (36.1.0)
faraday (>= 1.0.1, < 3.0)
faraday-multipart (>= 1.0.1, < 2.0)
platform_agent (1.0.1)
activesupport (>= 5.2.0)
useragent (~> 0.16.3)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
@@ -317,7 +344,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.9)
rack (3.1.11)
rack-mini-profiler (3.3.1)
rack (>= 1.2.0)
rack-session (2.1.0)
@@ -369,50 +396,58 @@ GEM
ffi (~> 1.0)
rbs (3.8.1)
logger
rdoc (6.11.0)
rdoc (6.12.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
redcarpet (3.6.1)
regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
rexml (3.4.0)
rubocop (1.71.0)
rexml (3.4.1)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rubocop (1.73.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.0)
rubocop-ast (1.38.1)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.29.1)
rubocop-performance (1.24.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.30.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails-omakase (1.0.0)
rubocop
rubocop-minitest
rubocop-performance
rubocop-rails
ruby-lsp (0.23.6)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails-omakase (1.1.0)
rubocop (>= 1.72)
rubocop-performance (>= 1.24)
rubocop-rails (>= 2.30)
ruby-lsp (0.23.9)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)
ruby-lsp-rails (0.3.31)
ruby-lsp-rails (0.4.0)
ruby-lsp (>= 0.23.0, < 0.24.0)
ruby-openai (7.4.0)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ruby-vips (2.2.3)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
@@ -421,16 +456,16 @@ GEM
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
securerandom (0.4.1)
selenium-webdriver (4.28.0)
selenium-webdriver (4.29.1)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sentry-rails (5.22.3)
sentry-rails (5.22.4)
railties (>= 5.0)
sentry-ruby (~> 5.22.3)
sentry-ruby (5.22.3)
sentry-ruby (~> 5.22.4)
sentry-ruby (5.22.4)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
simplecov (0.22.0)
@@ -440,27 +475,28 @@ GEM
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sorbet-runtime (0.5.11781)
stackprof (0.2.27)
sorbet-runtime (0.5.11813)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.2)
stripe (13.4.1)
tailwindcss-rails (3.3.1)
stringio (3.1.5)
stripe (13.5.0)
tailwindcss-rails (4.0.0)
railties (>= 7.0.0)
tailwindcss-ruby (~> 3.0)
tailwindcss-ruby (3.4.17-aarch64-linux)
tailwindcss-ruby (3.4.17-arm-linux)
tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux)
tailwindcss-ruby (~> 4.0)
tailwindcss-ruby (4.0.6)
tailwindcss-ruby (4.0.6-aarch64-linux-gnu)
tailwindcss-ruby (4.0.6-aarch64-linux-musl)
tailwindcss-ruby (4.0.6-arm64-darwin)
tailwindcss-ruby (4.0.6-x86_64-darwin)
tailwindcss-ruby (4.0.6-x86_64-linux-gnu)
tailwindcss-ruby (4.0.6-x86_64-linux-musl)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
thor (1.3.2)
timeout (0.4.3)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
@@ -470,12 +506,13 @@ GEM
useragent (0.16.11)
vcr (6.3.1)
base64
vernier (1.5.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.24.0)
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -486,7 +523,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.1)
zeitwerk (2.7.2)
PLATFORMS
aarch64-linux
@@ -519,7 +556,7 @@ DEPENDENCIES
faraday-retry
good_job
hotwire-livereload
hotwire_combobox
hotwire_combobox!
i18n-tasks
image_processing (>= 1.2)
importmap-rails
@@ -527,6 +564,7 @@ DEPENDENCIES
intercom-rails
jwt
letter_opener
logtail-rails
lucide-rails!
mocha
octokit
@@ -539,19 +577,22 @@ DEPENDENCIES
rails (~> 7.2.2)
rails-settings-cached
redcarpet
rotp (~> 6.3)
rqrcode (~> 2.2)
rubocop-rails-omakase
ruby-lsp-rails
ruby-openai
selenium-webdriver
sentry-rails
sentry-ruby
simplecov
stackprof
stimulus-rails
stripe
tailwindcss-rails
turbo-rails
tzinfo-data
vcr
vernier
web-console
webmock

View File

@@ -33,6 +33,15 @@ There are 3 primary ways to use the Maybe app:
2. [One-click deploy](docs/hosting/one-click-deploy.md)
3. [Self-host with Docker](docs/hosting/docker.md)
## Contributing
Before contributing, you'll likely find it helpful
to [understand context and general vision/direction](https://github.com/maybe-finance/maybe/wiki).
Once you've done that, please visit
our [contributing guide](https://github.com/maybe-finance/maybe/blob/main/CONTRIBUTING.md)
to get started!
## Local Development Setup
**If you are trying to _self-host_ the Maybe app, stop here. You
@@ -107,15 +116,6 @@ In development, we use `letter_opener` to automatically open emails in your
browser. When an email sends locally, a new browser tab will open with a
preview.
## Contributing
Before contributing, you'll likely find it helpful
to [understand context and general vision/direction](https://github.com/maybe-finance/maybe/wiki).
Once you've done that, please visit
our [contributing guide](https://github.com/maybe-finance/maybe/blob/main/CONTRIBUTING.md)
to get started!
## Repo Activity
![Repo Activity](https://repobeats.axiom.co/api/embed/7866c9790deba0baf63ca1688b209130b306ea4e.svg "Repobeats analytics image")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none">
<path d="M8.39804 24.0315H4.09584C3.07641 24.0315 2.25 24.8609 2.25 25.8841C2.25 26.9072 3.07641 27.7367 4.09584 27.7367H8.39804C9.41747 27.7367 10.2439 26.9072 10.2439 25.8841C10.2439 24.8609 9.41747 24.0315 8.39804 24.0315Z" fill="#F23E94"/>
<path d="M27.6403 27.7359H31.9425C32.9619 27.7359 33.7883 26.9065 33.7883 25.8833C33.7883 24.8601 32.9619 24.0307 31.9425 24.0307H27.6403C26.6209 24.0307 25.7945 24.8601 25.7945 25.8833C25.7945 26.9065 26.6209 27.7359 27.6403 27.7359Z" fill="#F23E94"/>
<path d="M19.7588 24.0189H16.2567C15.2373 24.0189 14.4109 24.8483 14.4109 25.8715C14.4109 26.8947 15.2373 27.7241 16.2567 27.7241H19.7588C20.7783 27.7241 21.6047 26.8947 21.6047 25.8715C21.6047 24.8483 20.7783 24.0189 19.7588 24.0189Z" fill="#F23E94"/>
<path d="M25.9683 22.4047H30.1112C31.1306 22.4047 31.957 21.5753 31.957 20.5521C31.957 19.529 31.1306 18.6995 30.1112 18.6995H25.9683C24.9489 18.6995 24.1225 19.529 24.1225 20.5521C24.1225 21.5753 24.9489 22.4047 25.9683 22.4047Z" fill="#6927DA"/>
<path d="M9.99971 18.6993H5.85685C4.83742 18.6993 4.01101 19.5288 4.01101 20.5519C4.01101 21.5751 4.83742 22.4045 5.85685 22.4045H9.99971C11.0191 22.4045 11.8455 21.5751 11.8455 20.5519C11.8455 19.5288 11.0191 18.6993 9.99971 18.6993Z" fill="#6927DA"/>
<path d="M21.0888 18.6875H14.924C13.9045 18.6875 13.0781 19.517 13.0781 20.5401C13.0781 21.5633 13.9045 22.3927 14.924 22.3927H21.0888C22.1082 22.3927 22.9346 21.5633 22.9346 20.5401C22.9346 19.517 22.1082 18.6875 21.0888 18.6875Z" fill="#6927DA"/>
<path d="M15.5578 13.2072H7.69136C6.67193 13.2072 5.84552 14.0366 5.84552 15.0598C5.84552 16.0829 6.67193 16.9123 7.69136 16.9123H15.5578C16.5772 16.9123 17.4036 16.0829 17.4036 15.0598C17.4036 14.0366 16.5772 13.2072 15.5578 13.2072Z" fill="#1570EF"/>
<path d="M20.9094 16.9116H28.2735C29.2929 16.9116 30.1193 16.0821 30.1193 15.059C30.1193 14.0358 29.2929 13.2064 28.2735 13.2064L20.9094 13.2064C19.89 13.2064 19.0636 14.0358 19.0636 15.059C19.0636 16.0821 19.89 16.9116 20.9094 16.9116Z" fill="#1570EF"/>
<path d="M26.5036 7.875H22.3515C21.3321 7.875 20.5057 8.70443 20.5057 9.72759C20.5057 10.7507 21.3321 11.5802 22.3515 11.5802H26.5036C27.523 11.5802 28.3494 10.7507 28.3494 9.72759C28.3494 8.70443 27.523 7.875 26.5036 7.875Z" fill="#22CCEE"/>
<path d="M13.6077 7.875H9.45557C8.43614 7.875 7.60973 8.70443 7.60973 9.72759C7.60973 10.7507 8.43614 11.5802 9.45557 11.5802H13.6077C14.6271 11.5802 15.4535 10.7507 15.4535 9.72759C15.4535 8.70443 14.6271 7.875 13.6077 7.875Z" fill="#22CCEE"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,194 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Reset rules, default styles applied to plain HTML */
@layer base {
details>summary::-webkit-details-marker {
@apply hidden;
}
details>summary {
@apply list-none;
}
}
@layer components {
.form-field {
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-white border-alpha-black-100 shadow-xs w-full;
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
}
.form-field__label, .hw-combobox__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
}
.form-field__input {
@apply border-none bg-transparent text-sm opacity-100 w-full p-0;
@apply focus:opacity-100 focus:outline-none focus:ring-0;
@apply placeholder-shown:opacity-50;
@apply disabled:text-gray-400;
@apply text-ellipsis overflow-hidden whitespace-nowrap;
}
select.form-field__input {
@apply pr-8;
}
.form-field__radio {
@apply text-gray-900;
}
.form-field__submit {
@apply cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
}
input:checked+label+.toggle-switch-dot {
transform: translateX(100%);
}
[type='checkbox'].maybe-checkbox {
@apply rounded-sm;
}
[type='checkbox'].maybe-checkbox--light {
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-500;
}
[type='checkbox'].maybe-checkbox--light:disabled {
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
}
[type='checkbox'].maybe-checkbox--dark {
@apply ring-gray-900 checked:text-white;
}
[type='checkbox'].maybe-checkbox--dark:disabled {
@apply cursor-not-allowed opacity-80 ring-gray-600;
}
[type='checkbox'].maybe-checkbox--dark:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
select[multiple="multiple"] {
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
}
select[multiple="multiple"] option {
@apply py-2 rounded-md;
}
select[multiple="multiple"] option:checked {
@apply after:content-['\2713'] bg-white after:text-gray-500 after:ml-2;
}
select[multiple="multiple"] option:active,
select[multiple="multiple"] option:focus {
@apply bg-white;
}
.maybe-switch {
@apply block bg-gray-100 w-9 h-5 rounded-full cursor-pointer;
@apply after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out;
@apply peer-checked:bg-green-600 peer-checked:after:translate-x-4;
}
.prose--github-release-notes {
.octicon {
@apply inline-block overflow-visible align-text-bottom fill-current;
}
.dropdown-caret {
@apply content-none border-4 border-b-0 border-transparent border-t-gray-500 size-0 inline-block;
}
.user-mention {
@apply font-bold;
}
}
.tooltip {
@apply hidden absolute;
}
.btn {
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
}
.btn--primary {
@apply bg-gray-900 text-white hover:bg-gray-700 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--secondary {
@apply bg-gray-50 hover:bg-gray-100 text-gray-900;
}
.btn--outline {
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--ghost {
@apply border border-transparent text-gray-900 hover:bg-gray-50;
}
}
.combobox {
.hw-combobox__main__wrapper, .hw-combobox__input {
@apply w-full;
}
.hw-combobox__main__wrapper {
@apply border-0 p-0 focus:border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none focus-within:shadow-none;
}
.hw-combobox__listbox {
@apply absolute top-[160%] right-0 w-full bg-transparent rounded z-30;
}
.hw_combobox__pagination__wrapper {
@apply h-px;
&:only-child {
@apply bg-transparent;
}
}
--hw-border-color: rgba(0, 0, 0, 0.2);
--hw-handle-width: 20px;
--hw-handle-height: 20px;
--hw-handle-offset-right: 0px;
}
/* Small, single purpose classes that should take precedence over other styles */
@layer utilities {
.scrollbar::-webkit-scrollbar {
width: 4px;
}
.scrollbar::-webkit-scrollbar-thumb {
background: #d6d6d6;
border-radius: 10px;
}
.scrollbar::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}
/* Custom scrollbar implementation for Windows browsers */
.windows {
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-thumb {
background: #d6d6d6;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,144 @@
@import 'tailwindcss';
@import "./maybe-design-system.css";
@import "./geist-font.css";
@import "./geist-mono-font.css";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@import "../stylesheets/simonweb_pickr.css";
@layer components {
.pcr-app{
position: static !important;
background: none !important;
box-shadow: none !important;
padding: 0 !important;
width: 100% !important;
}
.pcr-color-palette{
height: 12em !important;
width: 21.5rem !important;
}
.pcr-palette{
border-radius: 10px !important;
}
.pcr-palette:before{
border-radius: 10px !important;
}
.pcr-color-chooser{
height: 1.5em !important;
}
.pcr-picker{
height: 20px !important;
width: 20px !important;
}
}
.combobox {
.hw-combobox__main__wrapper,
.hw-combobox__input {
@apply w-full;
}
.hw-combobox__main__wrapper {
@apply border-0 p-0 focus:border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none focus-within:shadow-none;
}
.hw-combobox__listbox {
@apply absolute top-[160%] right-0 w-full bg-transparent rounded z-30;
}
.hw-combobox__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
}
.hw_combobox__pagination__wrapper {
@apply h-px;
&:only-child {
@apply bg-transparent;
}
}
--hw-border-color: rgba(0, 0, 0, 0.2);
--hw-handle-width: 20px;
--hw-handle-height: 20px;
--hw-handle-offset-right: 0px;
}
/* Typography */
.prose {
@apply max-w-none;
h2 {
@apply text-xl font-medium;
}
h3 {
@apply text-lg font-medium;
}
li {
@apply m-0;
}
details {
@apply mb-4 rounded-xl mt-3.5;
}
summary {
@apply flex items-center gap-1;
}
video {
@apply m-0 rounded-b-xl;
}
}
.prose--github-release-notes {
.octicon {
@apply inline-block overflow-visible align-text-bottom fill-current;
}
.dropdown-caret {
@apply content-none border-4 border-b-0 border-transparent border-t-gray-500 size-0 inline-block;
}
.user-mention {
@apply font-bold;
}
}
/* Custom scrollbar implementation for Windows browsers */
.windows {
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-thumb {
background: #d6d6d6;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}
.scrollbar {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #d6d6d6;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover {
background: #a6a6a6;
}
}

View File

@@ -0,0 +1,85 @@
/* Variable font */
@font-face {
font-family: 'Geist';
src: url('./geist/Geist[wght].woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
/* Static fonts (fallback) */
@supports not (font-variation-settings: normal) {
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-ExtraBold.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist';
src: url('./geist/Geist-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
}

View File

@@ -0,0 +1,83 @@
/* Variable font */
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono[wght].woff2') format('woff2-variations');
font-weight: 100 950;
font-style: normal;
font-display: swap;
}
/* Static fonts (fallback) */
@supports not (font-variation-settings: normal) {
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-UltraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('./geist_mono/GeistMono-UltraBlack.woff2') format('woff2');
font-weight: 950;
font-style: normal;
font-display: swap;
}
}

View File

@@ -0,0 +1,458 @@
/*
This file contains all of the Figma design tokens, components, etc. that
are used globally across the app.
One-off styling (3rd party overrides, etc.) should be done in the application.css file.
*/
@theme {
/* Font families */
--font-sans: 'Geist', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
/* Base colors */
--color-white: #ffffff;
--color-black: #0B0B0B;
--color-success: var(--color-green-600);
--color-warning: var(--color-yellow-600);
--color-destructive: var(--color-red-600);
/* Gray scale */
--color-gray-25: #FAFAFA;
--color-gray-50: #F7F7F7;
--color-gray-100: #F0F0F0;
--color-gray-200: #E7E7E7;
--color-gray-300: #CFCFCF;
--color-gray-400: #9E9E9E;
--color-gray-500: #737373;
--color-gray-600: #5C5C5C;
--color-gray-700: #363636;
--color-gray-800: #242424;
--color-gray-900: #171717;
--color-gray: var(--color-gray-500);
--color-gray-tint-5: --alpha(var(--color-gray-500) / 5%);
--color-gray-tint-10: --alpha(var(--color-gray-500) / 10%);
/* Alpha colors */
--color-alpha-white-25: --alpha(var(--color-white) / 3%);
--color-alpha-white-50: --alpha(var(--color-white) / 5%);
--color-alpha-white-100: --alpha(var(--color-white) / 8%);
--color-alpha-white-200: --alpha(var(--color-white) / 10%);
--color-alpha-white-300: --alpha(var(--color-white) / 15%);
--color-alpha-white-400: --alpha(var(--color-white) / 20%);
--color-alpha-white-500: --alpha(var(--color-white) / 30%);
--color-alpha-white-600: --alpha(var(--color-white) / 40%);
--color-alpha-white-700: --alpha(var(--color-white) / 50%);
--color-alpha-white-800: --alpha(var(--color-white) / 70%);
--color-alpha-white-900: --alpha(var(--color-white) / 85%);
--color-alpha-black-25: --alpha(var(--color-black) / 3%);
--color-alpha-black-50: --alpha(var(--color-black) / 5%);
--color-alpha-black-100: --alpha(var(--color-black) / 8%);
--color-alpha-black-200: --alpha(var(--color-black) / 10%);
--color-alpha-black-300: --alpha(var(--color-black) / 15%);
--color-alpha-black-400: --alpha(var(--color-black) / 20%);
--color-alpha-black-500: --alpha(var(--color-black) / 30%);
--color-alpha-black-600: --alpha(var(--color-black) / 40%);
--color-alpha-black-700: --alpha(var(--color-black) / 50%);
--color-alpha-black-800: --alpha(var(--color-black) / 70%);
--color-alpha-black-900: --alpha(var(--color-black) / 85%);
/* Red scale */
--color-red-25: #FFFBFB;
--color-red-50: #FFF1F0;
--color-red-100: #FFDEDB;
--color-red-200: #FEB9B3;
--color-red-300: #F88C86;
--color-red-400: #ED4E4E;
--color-red-500: #F13636;
--color-red-600: #EC2222;
--color-red-700: #C91313;
--color-red-800: #A40E0E;
--color-red-900: #7E0707;
--color-red-tint-5: --alpha(var(--color-red-500) / 5%);
--color-red-tint-10: --alpha(var(--color-red-500) / 10%);
/* Green scale */
--color-green-25: #F6FEF9;
--color-green-50: #ECFDF3;
--color-green-100: #D1FADF;
--color-green-200: #A6F4C5;
--color-green-300: #6CE9A6;
--color-green-400: #32D583;
--color-green-500: #12B76A;
--color-green-600: #10A861;
--color-green-700: #078C52;
--color-green-800: #05603A;
--color-green-900: #054F31;
--color-green-tint-5: --alpha(var(--color-green-500) / 5%);
--color-green-tint-10: --alpha(var(--color-green-500) / 10%);
/* Yellow scale */
--color-yellow-25: #FFFCF5;
--color-yellow-50: #FFFAEB;
--color-yellow-100: #FEF0C7;
--color-yellow-200: #FEDF89;
--color-yellow-300: #FEC84B;
--color-yellow-400: #FDB022;
--color-yellow-500: #F79009;
--color-yellow-600: #DC6803;
--color-yellow-700: #B54708;
--color-yellow-800: #93370D;
--color-yellow-900: #7A2E0E;
--color-yellow-tint-5: --alpha(var(--color-yellow-500) / 5%);
--color-yellow-tint-10: --alpha(var(--color-yellow-500) / 10%);
/* Cyan scale */
--color-cyan-25: #F5FEFF;
--color-cyan-50: #ECFDFF;
--color-cyan-100: #CFF9FE;
--color-cyan-200: #A5F0FC;
--color-cyan-300: #67E3F9;
--color-cyan-400: #22CCEE;
--color-cyan-500: #06AED4;
--color-cyan-600: #088AB2;
--color-cyan-700: #0E7090;
--color-cyan-800: #155B75;
--color-cyan-900: #155B75;
--color-cyan-tint-5: --alpha(var(--color-cyan-500) / 5%);
--color-cyan-tint-10: --alpha(var(--color-cyan-500) / 10%);
/* Blue scale */
--color-blue-25: #F5FAFF;
--color-blue-50: #EFF8FF;
--color-blue-100: #D1E9FF;
--color-blue-200: #B2DDFF;
--color-blue-300: #84CAFF;
--color-blue-400: #53B1FD;
--color-blue-500: #2E90FA;
--color-blue-600: #1570EF;
--color-blue-700: #175CD3;
--color-blue-800: #1849A9;
--color-blue-900: #194185;
--color-blue-tint-5: --alpha(var(--color-blue-500) / 5%);
--color-blue-tint-10: --alpha(var(--color-blue-500) / 10%);
/* Indigo scale */
--color-indigo-25: #F5F8FF;
--color-indigo-50: #EFF4FF;
--color-indigo-100: #E0EAFF;
--color-indigo-200: #C7D7FE;
--color-indigo-300: #A4BCFD;
--color-indigo-400: #8098F9;
--color-indigo-500: #6172F3;
--color-indigo-600: #444CE7;
--color-indigo-700: #3538CD;
--color-indigo-800: #2D31A6;
--color-indigo-900: #2D3282;
--color-indigo-tint-5: --alpha(var(--color-indigo-500) / 5%);
--color-indigo-tint-10: --alpha(var(--color-indigo-500) / 10%);
/* Violet scale */
--color-violet-25: #FBFAFF;
--color-violet-50: #F5F3FF;
--color-violet-100: #ECE9FE;
--color-violet-200: #DDD6FE;
--color-violet-300: #C3B5FD;
--color-violet-400: #A48AFB;
--color-violet-500: #875BF7;
--color-violet-600: #7839EE;
--color-violet-700: #6927DA;
--color-violet-tint-5: --alpha(var(--color-violet-500) / 5%);
--color-violet-tint-10: --alpha(var(--color-violet-500) / 10%);
/* Fuchsia scale */
--color-fuchsia-25: #FEFAFF;
--color-fuchsia-50: #FDF4FF;
--color-fuchsia-100: #FBE8FF;
--color-fuchsia-200: #F6D0FE;
--color-fuchsia-300: #EEAAFD;
--color-fuchsia-400: #E478FA;
--color-fuchsia-500: #D444F1;
--color-fuchsia-600: #BA24D5;
--color-fuchsia-700: #9F1AB1;
--color-fuchsia-800: #821890;
--color-fuchsia-900: #6F1877;
--color-fuchsia-tint-5: --alpha(var(--color-fuchsia-500) / 5%);
--color-fuchsia-tint-10: --alpha(var(--color-fuchsia-500) / 10%);
/* Pink scale */
--color-pink-25: #FFFAFC;
--color-pink-50: #FEF0F7;
--color-pink-100: #FFD1E2;
--color-pink-200: #FFB1CE;
--color-pink-300: #FD8FBA;
--color-pink-400: #F86BA7;
--color-pink-500: #F23E94;
--color-pink-600: #D5327F;
--color-pink-700: #BA256B;
--color-pink-800: #9E1958;
--color-pink-900: #840B45;
--color-pink-tint-5: --alpha(var(--color-pink-500) / 5%);
--color-pink-tint-10: --alpha(var(--color-pink-500) / 10%);
/* Orange scale */
--color-orange-25: #FFF9F5;
--color-orange-50: #FFF4ED;
--color-orange-100: #FFE6D5;
--color-orange-200: #FFD6AE;
--color-orange-300: #FF9C66;
--color-orange-400: #FF692E;
--color-orange-500: #FF4405;
--color-orange-600: #E62E05;
--color-orange-700: #BC1B06;
--color-orange-800: #97180C;
--color-orange-900: #771A0D;
--color-orange-tint-5: --alpha(var(--color-orange-500) / 5%);
--color-orange-tint-10: --alpha(var(--color-orange-500) / 10%);
/* Border radius overrides */
--border-radius-md: 8px;
--border-radius-lg: 10px;
--shadow-xs: 0px 1px 2px 0px --alpha(var(--color-black) / 6%);
--shadow-sm: 0px 1px 6px 0px --alpha(var(--color-black) / 6%);
--shadow-md: 0px 4px 8px -2px --alpha(var(--color-black) / 6%);
--shadow-lg: 0px 12px 16px -4px --alpha(var(--color-black) / 6%);
--shadow-xl: 0px 20px 24px -4px --alpha(var(--color-black) / 6%);
}
/* Custom shadow borders used for surfaces / containers */
@utility shadow-border-xs {
box-shadow: var(--shadow-xs), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-sm {
box-shadow: var(--shadow-sm), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-md {
box-shadow: var(--shadow-md), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-lg {
box-shadow: var(--shadow-lg), 0px 0px 0px 1px var(--color-alpha-black-50);
}
@utility shadow-border-xl {
box-shadow: var(--shadow-xl), 0px 0px 0px 1px var(--color-alpha-black-50);
}
/* Design system color utilities */
@utility text-primary {
@apply text-gray-900;
}
@utility text-secondary {
@apply text-gray-500;
}
@utility text-subdued {
@apply text-gray-400;
}
@utility text-link {
@apply text-blue-600;
}
@utility bg-surface {
@apply bg-gray-50;
}
@utility bg-surface-hover {
@apply bg-gray-100;
}
@utility bg-surface-inset {
@apply bg-gray-100;
}
@utility bg-surface-inset-hover {
@apply bg-gray-200;
}
@utility bg-container {
@apply bg-white;
}
@utility bg-container-hover {
@apply bg-gray-50;
}
@utility bg-container-inset {
@apply bg-gray-50;
}
@utility bg-container-inset-hover {
@apply bg-gray-100;
}
@utility bg-inverse {
@apply bg-gray-800;
}
@utility bg-inverse-hover {
@apply bg-gray-700;
}
@utility bg-overlay {
@apply bg-alpha-black-200;
}
@utility border-primary {
@apply border-alpha-black-300;
}
@utility border-secondary {
@apply border-alpha-black-200;
}
@utility border-tertiary {
@apply border-alpha-black-100;
}
@utility border-subdued {
@apply border-alpha-black-50;
}
@layer base {
form>button {
@apply cursor-pointer;
}
hr {
@apply text-gray-200;
}
details>summary::-webkit-details-marker {
@apply hidden;
}
details>summary {
@apply list-none;
}
}
@layer components {
/* Buttons */
.btn {
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
@apply transition-all duration-300;
}
.btn--primary {
@apply bg-gray-900 text-white hover:bg-gray-700 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--secondary {
@apply bg-gray-50 hover:bg-gray-100 text-gray-900;
}
.btn--outline {
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
}
.btn--ghost {
@apply border border-transparent text-gray-900 hover:bg-gray-100;
}
.btn--destructive {
@apply bg-red-500 text-white hover:bg-red-600 disabled:bg-red-50 disabled:hover:bg-red-50 disabled:text-red-400;
}
/* Forms */
.form-field {
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-white border-alpha-black-100 shadow-xs w-full;
@apply focus-within:border-gray-900 focus-within:shadow-none focus-within:ring-4 focus-within:ring-gray-100;
@apply transition-all duration-300;
/* Add styles for multiple select within form fields */
select[multiple] {
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
option {
@apply py-2 rounded-md;
}
option:checked {
@apply after:content-['\2713'] bg-white after:text-gray-500 after:ml-2;
}
option:active,
option:focus {
@apply bg-white;
}
}
}
.form-field__label {
@apply block text-xs text-gray-500 peer-disabled:text-gray-400;
}
.form-field__input {
@apply border-none bg-transparent text-sm opacity-100 w-full p-0;
@apply focus:opacity-100 focus:outline-hidden focus:ring-0;
@apply placeholder-shown:opacity-50;
@apply disabled:text-gray-400;
@apply text-ellipsis overflow-hidden whitespace-nowrap;
@apply transition-opacity duration-300;
&select {
@apply pr-8;
}
}
.form-field__radio {
@apply text-gray-900;
}
.form-field__submit {
@apply cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700;
}
/* Checkboxes */
.checkbox {
&[type='checkbox'] {
@apply rounded-sm;
@apply transition-colors duration-300;
}
}
.checkbox--light {
&[type='checkbox'] {
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-500;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
}
}
.checkbox--dark {
&[type='checkbox'] {
@apply ring-gray-900 checked:text-white;
}
&[type='checkbox']:disabled {
@apply cursor-not-allowed opacity-80 ring-gray-600;
}
&[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
}
/* Switches */
.switch {
@apply block bg-gray-100 w-9 h-5 rounded-full cursor-pointer;
@apply after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full;
@apply after:transition-transform after:duration-300 after:ease-in-out;
@apply peer-checked:bg-green-600 peer-checked:after:translate-x-4;
@apply transition-colors duration-300;
}
/* Tooltips */
.tooltip {
@apply hidden absolute;
}
}

View File

@@ -1,6 +1,4 @@
class Account::HoldingsController < ApplicationController
layout :with_sidebar
before_action :set_holding, only: %i[show destroy]
def index
@@ -11,9 +9,12 @@ class Account::HoldingsController < ApplicationController
end
def destroy
@holding.destroy_holding_and_entries!
flash[:notice] = t(".success")
if @holding.account.plaid_account_id.present?
flash[:alert] = "You cannot delete this holding"
else
@holding.destroy_holding_and_entries!
flash[:notice] = t(".success")
end
respond_to do |format|
format.html { redirect_back_or_to account_path(@holding.account) }

View File

@@ -10,7 +10,7 @@ class Account::TradesController < ApplicationController
def create_entry_params
params.require(:account_entry).permit(
:account_id, :date, :amount, :currency, :qty, :price, :ticker, :type, :transfer_account_id
:account_id, :date, :amount, :currency, :qty, :price, :ticker, :manual_ticker, :type, :transfer_account_id
).tap do |params|
account_id = params.delete(:account_id)
params[:account] = Current.family.accounts.find(account_id)

View File

@@ -3,7 +3,7 @@ class Account::TransferMatchesController < ApplicationController
def new
@accounts = Current.family.accounts.alphabetically.where.not(id: @entry.account_id)
@transfer_match_candidates = @entry.transfer_match_candidates
@transfer_match_candidates = @entry.account_transaction.transfer_match_candidates
end
def create

View File

@@ -0,0 +1,25 @@
class AccountableSparklinesController < ApplicationController
def show
@accountable = Accountable.from_type(params[:accountable_type]&.classify)
@series = Rails.cache.fetch(cache_key) do
family.accounts.active
.where(accountable_type: @accountable.name)
.balance_series(
currency: family.currency,
favorable_direction: @accountable.favorable_direction
)
end
render layout: false
end
private
def family
Current.family
end
def cache_key
family.build_cache_key("#{@accountable.name}_sparkline")
end
end

View File

@@ -1,26 +1,12 @@
class AccountsController < ApplicationController
layout :with_sidebar
before_action :set_account, only: %i[sync]
before_action :set_account, only: %i[sync chart sparkline]
include Periodable
def index
@manual_accounts = Current.family.accounts.manual.alphabetically
@plaid_items = Current.family.plaid_items.ordered
end
@manual_accounts = family.accounts.manual.alphabetically
@plaid_items = family.plaid_items.ordered
def summary
@period = Period.from_param(params[:period])
snapshot = Current.family.snapshot(@period)
@net_worth_series = snapshot[:net_worth_series]
@asset_series = snapshot[:asset_series]
@liability_series = snapshot[:liability_series]
@accounts = Current.family.accounts.active
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
end
def list
@period = Period.from_param(params[:period])
render layout: false
render layout: "settings"
end
def sync
@@ -32,20 +18,28 @@ class AccountsController < ApplicationController
end
def chart
@account = Current.family.accounts.find(params[:id])
@chart_view = params[:chart_view] || "balance"
render layout: "application"
end
def sparkline
render layout: false
end
def sync_all
unless Current.family.syncing?
Current.family.sync_later
unless family.syncing?
family.sync_later
end
redirect_to accounts_path
end
private
def family
Current.family
end
def set_account
@account = Current.family.accounts.find(params[:id])
@account = family.accounts.find(params[:id])
end
end

View File

@@ -1,5 +1,5 @@
class ApplicationController < ActionController::Base
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable, Breadcrumbable
include Pagy::Backend
helper_method :require_upgrade?, :subscription_pending?
@@ -22,12 +22,6 @@ class ApplicationController < ActionController::Base
subscribed_at.present? && subscribed_at <= Time.current && subscribed_at > 1.hour.ago
end
def with_sidebar
return "turbo_rails/frame" if turbo_frame_request?
"with_sidebar"
end
def detect_os
user_agent = request.user_agent
@os = case user_agent

View File

@@ -1,13 +1,13 @@
class BudgetCategoriesController < ApplicationController
before_action :set_budget
def index
@budget = Current.family.budgets.find(params[:budget_id])
@budget_categories = @budget.budget_categories.includes(:category)
render layout: "wizard"
end
def show
@budget = Current.family.budgets.find(params[:budget_id])
@recent_transactions = @budget.entries
@recent_transactions = @budget.transactions
if params[:id] == BudgetCategory.uncategorized.id
@budget_category = @budget.uncategorized_budget_category
@@ -23,13 +23,26 @@ class BudgetCategoriesController < ApplicationController
def update
@budget_category = Current.family.budget_categories.find(params[:id])
@budget_category.update!(budget_category_params)
redirect_to budget_budget_categories_path(@budget_category.budget)
if @budget_category.update(budget_category_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to budget_budget_categories_path(@budget) }
end
else
render :index, status: :unprocessable_entity
end
end
private
def budget_category_params
params.require(:budget_category).permit(:budgeted_spending)
params.require(:budget_category).permit(:budgeted_spending).tap do |params|
params[:budgeted_spending] = params[:budgeted_spending].presence || 0
end
end
def set_budget
start_date = Budget.param_to_date(params[:budget_month_year])
@budget = Current.family.budgets.find_by(start_date: start_date)
end
end

View File

@@ -6,10 +6,6 @@ class BudgetsController < ApplicationController
end
def show
@next_budget = @budget.next_budget
@previous_budget = @budget.previous_budget
@latest_budget = Budget.find_or_bootstrap(Current.family)
render layout: with_sidebar
end
def edit
@@ -21,12 +17,6 @@ class BudgetsController < ApplicationController
redirect_to budget_budget_categories_path(@budget)
end
def create
start_date = Date.parse(budget_create_params[:start_date])
@budget = Budget.find_or_bootstrap(Current.family, date: start_date)
redirect_to budget_path(@budget)
end
def picker
render partial: "budgets/picker", locals: {
family: Current.family,
@@ -35,6 +25,7 @@ class BudgetsController < ApplicationController
end
private
def budget_create_params
params.require(:budget).permit(:start_date)
end
@@ -44,12 +35,13 @@ class BudgetsController < ApplicationController
end
def set_budget
@budget = Current.family.budgets.find(params[:id])
@budget.sync_budget_categories
start_date = Budget.param_to_date(params[:month_year])
@budget = Budget.find_or_bootstrap(Current.family, start_date: start_date)
raise ActiveRecord::RecordNotFound unless @budget
end
def redirect_to_current_month_budget
current_budget = Budget.find_or_bootstrap(Current.family)
current_budget = Budget.find_or_bootstrap(Current.family, start_date: Date.current)
redirect_to budget_path(current_budget)
end
end

View File

@@ -1,12 +1,12 @@
class CategoriesController < ApplicationController
layout :with_sidebar
before_action :set_category, only: %i[edit update destroy]
before_action :set_categories, only: %i[update edit]
before_action :set_transaction, only: :create
def index
@categories = Current.family.categories.alphabetically
render layout: "settings"
end
def new

View File

@@ -1,6 +1,4 @@
class Category::DeletionsController < ApplicationController
layout :with_sidebar
before_action :set_category
before_action :set_replacement_category, only: :create

View File

@@ -0,0 +1,17 @@
class ChatsController < ApplicationController
def index
Current.user.update!(current_chat: nil)
@chats = Current.user.chats.ordered
end
def create
@chat = Current.user.chats.create_with_defaults!
redirect_to chat_path(@chat)
end
def show
@chat = Current.user.chats.find(params[:id])
Current.user.update!(current_chat: @chat)
end
end

View File

@@ -2,9 +2,8 @@ module AccountableResource
extend ActiveSupport::Concern
included do
include ScrollFocusable
include ScrollFocusable, Periodable
layout :with_sidebar
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
before_action :set_link_token, only: :new
end
@@ -24,6 +23,7 @@ module AccountableResource
end
def show
@chart_view = params[:chart_view] || "balance"
@q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological
@@ -53,7 +53,7 @@ module AccountableResource
private
def set_link_token
@us_link_token = Current.family.get_link_token(
webhooks_url: webhooks_url,
webhooks_url: plaid_us_webhooks_url,
redirect_url: accounts_url,
accountable_type: accountable_type.name,
region: :us
@@ -61,7 +61,7 @@ module AccountableResource
if Current.family.eu?
@eu_link_token = Current.family.get_link_token(
webhooks_url: webhooks_url,
webhooks_url: plaid_eu_webhooks_url,
redirect_url: accounts_url,
accountable_type: accountable_type.name,
region: :eu
@@ -69,11 +69,16 @@ module AccountableResource
end
end
def webhooks_url
def plaid_us_webhooks_url
return webhooks_plaid_url if Rails.env.production?
base_url = ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/"))
base_url + "/webhooks/plaid"
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid"
end
def plaid_eu_webhooks_url
return webhooks_plaid_eu_url if Rails.env.production?
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid_eu"
end
def accountable_type

View File

@@ -4,11 +4,13 @@ module Authentication
included do
before_action :set_request_details
before_action :authenticate_user!
before_action :set_sentry_user
end
class_methods do
def skip_authentication(**options)
skip_before_action :authenticate_user!, **options
skip_before_action :set_sentry_user, **options
end
end
@@ -26,7 +28,13 @@ module Authentication
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_token])
cookie_value = cookies.signed[:session_token]
if cookie_value.present?
Session.find_by(id: cookie_value)
else
nil
end
end
def create_session_for(user)
@@ -43,4 +51,17 @@ module Authentication
Current.user_agent = request.user_agent
Current.ip_address = request.ip
end
def set_sentry_user
return unless defined?(Sentry) && ENV["SENTRY_DSN"].present?
if Current.user
Sentry.set_user(
id: Current.user.id,
email: Current.user.email,
username: Current.user.display_name,
ip_address: Current.ip_address
)
end
end
end

View File

@@ -13,9 +13,8 @@ module AutoSync
def family_needs_auto_sync?
return false unless Current.family.present?
return false unless Current.family.accounts.any?
return false unless Current.family.accounts.active.any?
Current.family.last_synced_at.blank? ||
Current.family.last_synced_at.to_date < Date.current
(Current.family.last_synced_at&.to_date || 1.day.ago) < Date.current
end
end

View File

@@ -0,0 +1,13 @@
module Breadcrumbable
extend ActiveSupport::Concern
included do
before_action :set_breadcrumbs
end
private
# The default, unless specific controller or action explicitly overrides
def set_breadcrumbs
@breadcrumbs = [ [ "Home", root_path ], [ controller_name.titleize, nil ] ]
end
end

View File

@@ -2,7 +2,6 @@ module EntryableResource
extend ActiveSupport::Concern
included do
layout :with_sidebar
before_action :set_entry, only: %i[show update destroy]
end

View File

@@ -0,0 +1,14 @@
module Periodable
extend ActiveSupport::Concern
included do
before_action :set_period
end
private
def set_period
@period = Period.from_key(params[:period] || Current.user&.default_period)
rescue Period::InvalidKeyError
@period = Period.last_30_days
end
end

View File

@@ -1,11 +0,0 @@
class Help::ArticlesController < ApplicationController
layout :with_sidebar
def show
@article = Help::Article.find(params[:id])
unless @article
head :not_found
end
end
end

View File

@@ -29,11 +29,13 @@ class Import::ConfigurationsController < ApplicationController
:account_col_label,
:qty_col_label,
:ticker_col_label,
:exchange_operating_mic_col_label,
:price_col_label,
:entity_type_col_label,
:notes_col_label,
:currency_col_label,
:date_format,
:number_format,
:signage_convention
)
end

View File

@@ -4,6 +4,10 @@ class Import::ConfirmsController < ApplicationController
before_action :set_import
def show
if @import.mapping_steps.empty?
return redirect_to import_path(@import)
end
redirect_to import_clean_path(@import), alert: "You have invalid data, please edit until all errors are resolved" unless @import.cleaned?
end

View File

@@ -2,9 +2,7 @@ class Import::RowsController < ApplicationController
before_action :set_import_row
def update
@row.assign_attributes(row_params)
@row.save!(validate: false)
@row.sync_mappings
@row.update_and_sync(row_params)
redirect_to import_row_path(@row.import, @row)
end

View File

@@ -8,10 +8,11 @@ class Import::UploadsController < ApplicationController
def update
if csv_valid?(csv_str)
@import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))
@import.assign_attributes(raw_file_str: csv_str, col_sep: upload_params[:col_sep])
@import.save!(validate: false)
redirect_to import_configuration_path(@import), notice: "CSV uploaded successfully."
redirect_to import_configuration_path(@import, template_hint: true), notice: "CSV uploaded successfully."
else
flash.now[:alert] = "Must be valid CSV with headers and at least one row of data"
@@ -29,10 +30,8 @@ class Import::UploadsController < ApplicationController
end
def csv_valid?(str)
require "csv"
begin
csv = CSV.parse(str || "", headers: true, col_sep: upload_params[:col_sep])
csv = Import.parse_csv_str(str, col_sep: upload_params[:col_sep])
return false if csv.headers.empty?
return false if csv.count == 0
true

View File

@@ -1,5 +1,5 @@
class ImportsController < ApplicationController
before_action :set_import, only: %i[show publish destroy]
before_action :set_import, only: %i[show publish destroy revert apply_template]
def publish
@import.publish_later
@@ -10,7 +10,7 @@ class ImportsController < ApplicationController
def index
@imports = Current.family.imports
render layout: with_sidebar
render layout: "settings"
end
def new
@@ -18,7 +18,12 @@ class ImportsController < ApplicationController
end
def create
import = Current.family.imports.create! import_params
account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))
import = Current.family.imports.create!(
type: import_params[:type],
account: account,
date_format: Current.family.date_format,
)
redirect_to import_upload_path(import)
end
@@ -31,6 +36,20 @@ class ImportsController < ApplicationController
end
end
def revert
@import.revert_later
redirect_to imports_path, notice: "Import is reverting in the background."
end
def apply_template
if @import.suggested_template
@import.apply_template!(@import.suggested_template)
redirect_to import_configuration_path(@import), notice: "Template applied."
else
redirect_to import_configuration_path(@import), alert: "No template found, please manually configure your import."
end
end
def destroy
@import.destroy

View File

@@ -1,10 +1,10 @@
class MerchantsController < ApplicationController
layout :with_sidebar
before_action :set_merchant, only: %i[edit update destroy]
def index
@merchants = Current.family.merchants.alphabetically
render layout: "settings"
end
def new

View File

@@ -0,0 +1,23 @@
class MessagesController < ApplicationController
before_action :set_chat
def create
@message = @chat.messages.create!(message_params.merge(role: "user"))
AiResponseJob.perform_later(@message)
respond_to do |format|
format.turbo_stream
format.html { redirect_to chat_path(@chat) }
end
end
private
def set_chat
@chat = Current.user.chats.find(params[:chat_id])
end
def message_params
params.require(:message).permit(:content)
end
end

View File

@@ -0,0 +1,56 @@
class MfaController < ApplicationController
layout :determine_layout
skip_authentication only: [ :verify, :verify_code ]
def new
redirect_to root_path if Current.user.otp_required?
Current.user.setup_mfa! unless Current.user.otp_secret.present?
end
def create
if Current.user.verify_otp?(params[:code])
Current.user.enable_mfa!
@backup_codes = Current.user.otp_backup_codes
render :backup_codes
else
Current.user.disable_mfa!
redirect_to new_mfa_path, alert: t(".invalid_code")
end
end
def verify
@user = User.find_by(id: session[:mfa_user_id])
if @user.nil?
redirect_to new_session_path
end
end
def verify_code
@user = User.find_by(id: session[:mfa_user_id])
if @user&.verify_otp?(params[:code])
session.delete(:mfa_user_id)
@session = create_session_for(@user)
redirect_to root_path
else
flash.now[:alert] = t(".invalid_code")
render :verify, status: :unprocessable_entity
end
end
def disable
Current.user.disable_mfa!
redirect_to settings_security_path, notice: t(".success")
end
private
def determine_layout
if action_name.in?(%w[verify verify_code])
"auth"
else
"settings"
end
end
end

View File

@@ -1,5 +1,4 @@
class OnboardingsController < ApplicationController
layout "application"
before_action :set_user
before_action :load_invitation

View File

@@ -1,40 +1,22 @@
class PagesController < ApplicationController
skip_before_action :authenticate_user!, only: %i[early_access]
layout :with_sidebar, except: %i[early_access]
include Periodable
def dashboard
@period = Period.from_param(params[:period])
snapshot = Current.family.snapshot(@period)
@net_worth_series = snapshot[:net_worth_series]
@asset_series = snapshot[:asset_series]
@liability_series = snapshot[:liability_series]
@balance_sheet = Current.family.balance_sheet
@accounts = Current.family.accounts.active.with_attached_logo
snapshot_transactions = Current.family.snapshot_transactions
@income_series = snapshot_transactions[:income_series]
@spending_series = snapshot_transactions[:spending_series]
@savings_rate_series = snapshot_transactions[:savings_rate_series]
snapshot_account_transactions = Current.family.snapshot_account_transactions
@top_spenders = snapshot_account_transactions[:top_spenders]
@top_earners = snapshot_account_transactions[:top_earners]
@top_savers = snapshot_account_transactions[:top_savers]
@accounts = Current.family.accounts.active
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
@transaction_entries = Current.family.entries.incomes_and_expenses.limit(6).reverse_chronological
# TODO: Placeholders for trendlines
placeholder_series_data = 10.times.map do |i|
{ date: Date.current - i.days, value: Money.new(0, Current.family.currency) }
end
@investing_series = TimeSeries.new(placeholder_series_data)
@breadcrumbs = [ [ "Home", root_path ], [ "Dashboard", nil ] ]
end
def changelog
@release_notes = Provider::Github.new.fetch_latest_release_notes
render layout: "settings"
end
def feedback
render layout: "settings"
end
def early_access

View File

@@ -21,7 +21,10 @@ class PlaidItemsController < ApplicationController
@plaid_item.sync_later
end
redirect_to accounts_path
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
format.json { head :ok }
end
end
private

View File

@@ -1,10 +1,7 @@
class SecuritiesController < ApplicationController
def index
query = params[:q]
return render json: [] if query.blank? || query.length < 2 || query.length > 100
@securities = Security.search({
search: query,
@securities = Security.search_provider({
search: params[:q],
country: params[:country_code] == "US" ? "US" : nil
})
end

View File

@@ -9,8 +9,13 @@ class SessionsController < ApplicationController
def create
if user = User.authenticate_by(email: params[:email], password: params[:password])
@session = create_session_for(user)
redirect_to root_path
if user.otp_required?
session[:mfa_user_id] = user.id
redirect_to verify_mfa_path
else
@session = create_session_for(user)
redirect_to root_path
end
else
flash.now[:alert] = t(".invalid_credentials")
render :new, status: :unprocessable_entity

View File

@@ -1,4 +1,6 @@
class Settings::BillingsController < SettingsController
class Settings::BillingsController < ApplicationController
layout "settings"
def show
@user = Current.user
end

View File

@@ -1,5 +1,8 @@
class Settings::HostingsController < SettingsController
class Settings::HostingsController < ApplicationController
layout "settings"
before_action :raise_if_not_self_hosted
before_action :ensure_admin, only: :clear_cache
def show
@synth_usage = Current.family.synth_usage
@@ -36,6 +39,11 @@ class Settings::HostingsController < SettingsController
render :show, status: :unprocessable_entity
end
def clear_cache
DataCacheClearJob.perform_later(Current.family)
redirect_to settings_hosting_path, notice: t(".cache_cleared")
end
private
def hosting_params
params.require(:setting).permit(:render_deploy_hook, :upgrades_setting, :require_invite_for_signup, :require_email_confirmation, :synth_api_key)
@@ -44,4 +52,8 @@ class Settings::HostingsController < SettingsController
def raise_if_not_self_hosted
raise "Settings not available on non-self-hosted instance" unless self_hosted?
end
def ensure_admin
redirect_to settings_hosting_path, alert: t(".not_authorized") unless Current.user.admin?
end
end

View File

@@ -1,4 +1,6 @@
class Settings::PreferencesController < SettingsController
class Settings::PreferencesController < ApplicationController
layout "settings"
def show
@user = Current.user
end

View File

@@ -1,4 +1,6 @@
class Settings::ProfilesController < SettingsController
class Settings::ProfilesController < ApplicationController
layout "settings"
def show
@user = Current.user
@users = Current.family.users.order(:created_at)

View File

@@ -0,0 +1,6 @@
class Settings::SecuritiesController < ApplicationController
layout "settings"
def show
end
end

View File

@@ -1,3 +0,0 @@
class SettingsController < ApplicationController
layout :with_sidebar
end

View File

@@ -1,4 +1,6 @@
class SubscriptionsController < ApplicationController
before_action :redirect_to_root_if_self_hosted
def new
if Current.family.stripe_customer_id.blank?
customer = stripe_client.v1.customers.create(
@@ -44,4 +46,8 @@ class SubscriptionsController < ApplicationController
def stripe_client
@stripe_client ||= Stripe::StripeClient.new(ENV["STRIPE_SECRET_KEY"])
end
def redirect_to_root_if_self_hosted
redirect_to root_path, alert: I18n.t("subscriptions.self_hosted_alert") if self_hosted?
end
end

View File

@@ -1,6 +1,4 @@
class Tag::DeletionsController < ApplicationController
layout :with_sidebar
before_action :set_tag
before_action :set_replacement_tag, only: :create

View File

@@ -1,10 +1,10 @@
class TagsController < ApplicationController
layout :with_sidebar
before_action :set_tag, only: %i[edit update destroy]
def index
@tags = Current.family.tags.alphabetically
render layout: "settings"
end
def new

View File

@@ -1,34 +1,26 @@
class TransactionsController < ApplicationController
include ScrollFocusable
layout :with_sidebar
before_action :store_params!, only: :index
def index
@q = search_params
search_query = Current.family.transactions.search(@q)
transactions_query = Current.family.transactions.active.search(@q)
set_focused_record(search_query, params[:focused_record_id], default_per_page: 50)
set_focused_record(transactions_query, params[:focused_record_id], default_per_page: 50)
@pagy, @transaction_entries = pagy(
search_query.reverse_chronological.preload(
:account,
entryable: [
:category, :merchant, :tags,
:transfer_as_inflow,
transfer_as_outflow: {
inflow_transaction: { entry: :account },
outflow_transaction: { entry: :account }
}
]
),
@pagy, @transactions = pagy(
transactions_query.includes(
{ entry: :account },
:category, :merchant, :tags,
transfer_as_outflow: { inflow_transaction: { entry: :account } },
transfer_as_inflow: { outflow_transaction: { entry: :account } }
).reverse_chronological,
limit: params[:per_page].presence || default_params[:per_page],
params: ->(params) { params.except(:focused_record_id) }
)
@transfers = @transaction_entries.map { |entry| entry.entryable.transfer_as_outflow }.compact
@totals = search_query.stats(Current.family.currency)
@totals = Current.family.income_statement.totals(transactions_scope: transactions_query)
end
def clear_filter
@@ -57,6 +49,7 @@ class TransactionsController < ApplicationController
end
private
def search_params
cleaned_params = params.fetch(:q, {})
.permit(

View File

@@ -1,6 +1,4 @@
class TransfersController < ApplicationController
layout :with_sidebar
before_action :set_transfer, only: %i[destroy show update]
def new

View File

@@ -1,5 +1,6 @@
class UsersController < ApplicationController
before_action :set_user
before_action :ensure_admin, only: :reset
def update
@user = Current.user
@@ -19,10 +20,18 @@ class UsersController < ApplicationController
@user.update!(user_params.except(:redirect_to, :delete_profile_image))
@user.profile_image.purge if should_purge_profile_image?
handle_redirect(t(".success"))
respond_to do |format|
format.html { handle_redirect(t(".success")) }
format.json { head :ok }
end
end
end
def reset
FamilyResetJob.perform_later(Current.family)
redirect_to settings_profile_path, notice: t(".success")
end
def destroy
if @user.deactivate
Current.session.destroy
@@ -57,7 +66,7 @@ class UsersController < ApplicationController
def user_params
params.require(:user).permit(
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
:first_name, :last_name, :email, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at, :show_sidebar, :default_period,
family_attributes: [ :name, :currency, :country, :locale, :date_format, :timezone, :id, :data_enrichment_enabled ]
)
end
@@ -65,4 +74,8 @@ class UsersController < ApplicationController
def set_user
@user = Current.user
end
def ensure_admin
redirect_to settings_profile_path, alert: I18n.t("users.reset.unauthorized") unless Current.user.admin?
end
end

View File

@@ -6,11 +6,29 @@ class WebhooksController < ApplicationController
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]
Provider::Plaid.validate_webhook!(plaid_verification_header, webhook_body)
Provider::Plaid.process_webhook(webhook_body)
client = Provider::Plaid.new(Rails.application.config.plaid, region: :us)
client.validate_webhook!(plaid_verification_header, webhook_body)
client.process_webhook(webhook_body)
render json: { received: true }, status: :ok
rescue => error
Sentry.capture_exception(error)
render json: { error: "Invalid webhook: #{error.message}" }, status: :bad_request
end
def plaid_eu
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]
client = Provider::Plaid.new(Rails.application.config.plaid_eu, region: :eu)
client.validate_webhook!(plaid_verification_header, webhook_body)
client.process_webhook(webhook_body)
render json: { received: true }, status: :ok
rescue => error
Sentry.capture_exception(error)
render json: { error: "Invalid webhook: #{error.message}" }, status: :bad_request
end
@@ -33,10 +51,12 @@ class WebhooksController < ApplicationController
Rails.logger.info "Unhandled event type: #{event.type}"
end
rescue JSON::ParserError
rescue JSON::ParserError => error
Sentry.capture_exception(error)
render json: { error: "Invalid payload" }, status: :bad_request
return
rescue Stripe::SignatureVerificationError
rescue Stripe::SignatureVerificationError => error
Sentry.capture_exception(error)
render json: { error: "Invalid signature" }, status: :bad_request
return
end

View File

@@ -1,22 +1,31 @@
module Account::EntriesHelper
def permitted_entryable_partial_path(entry, relative_partial_path)
"account/entries/entryables/#{permitted_entryable_key(entry)}/#{relative_partial_path}"
end
def entries_by_date(entries, totals: false)
transfer_groups = entries.group_by do |entry|
# Only check for transfer if it's a transaction
next nil unless entry.entryable_type == "Account::Transaction"
entry.entryable.transfer&.id
end
def transfer_entries(entries)
transfers = entries.select { |e| e.transfer_id.present? }
transfers.map(&:transfer).uniq
end
# For a more intuitive UX, we do not want to show the same transfer twice in the list
deduped_entries = transfer_groups.flat_map do |transfer_id, grouped_entries|
if transfer_id.nil? || grouped_entries.size == 1
grouped_entries
else
grouped_entries.reject do |e|
e.entryable_type == "Account::Transaction" &&
e.entryable.transfer_as_inflow.present?
end
end
end
def entries_by_date(entries, transfers: [], selectable: true, totals: false)
entries.group_by(&:date).map do |date, grouped_entries|
deduped_entries.group_by(&:date).map do |date, grouped_entries|
content = capture do
yield [ grouped_entries, transfers.select { |t| t.outflow_transaction.entry.date == date } ]
yield grouped_entries
end
next if content.blank?
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, selectable:, totals: }
render partial: "account/entries/entry_group", locals: { date:, entries: grouped_entries, content:, totals: }
end.compact.join.html_safe
end
@@ -28,11 +37,4 @@ module Account::EntriesHelper
entry.display_name
].join("")
end
private
def permitted_entryable_key(entry)
permitted_entryable_paths = %w[transaction valuation trade]
entry.entryable_name_short.presence_in(permitted_entryable_paths)
end
end

View File

@@ -1,13 +0,0 @@
module Account::HoldingsHelper
def brokerage_cash_holding(account)
currency = Money::Currency.new(account.currency)
account.holdings.build \
date: Date.current,
qty: account.cash_balance,
price: 1,
amount: account.cash_balance,
currency: currency.iso_code,
security: Security.new(ticker: currency.iso_code, name: currency.name)
end
end

View File

@@ -1,87 +1,6 @@
module AccountsHelper
def period_label(period)
return "since account creation" if period.date_range.begin.nil?
start_date, end_date = period.date_range.first, period.date_range.last
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
days_apart = (end_date - start_date).to_i
# Handle specific cases
if start_date == Date.current.beginning_of_week && end_date == Date.current
return "Current Week to Date (CWD)"
elsif start_date == Date.current.beginning_of_month && end_date == Date.current
return "Current Month to Date (MTD)"
elsif start_date == Date.current.beginning_of_quarter && end_date == Date.current
return "Current Quarter to Date (CQD)"
elsif start_date == Date.current.beginning_of_year && end_date == Date.current
return "Current Year to Date (YTD)"
end
# Default cases
case days_apart
when 1
"vs. yesterday"
when 7
"vs. last week"
when 30, 31
"vs. last month"
when 90
"vs. last 3 months"
when 365, 366
"vs. last year"
else
"from #{start_date.strftime('%b %d, %Y')} to #{end_date.strftime('%b %d, %Y')}"
end
end
def summary_card(title:, &block)
content = capture(&block)
render "accounts/summary_card", title: title, content: content
end
def to_accountable_title(accountable)
accountable.model_name.human
end
def accountable_text_class(accountable_type)
class_mapping(accountable_type)[:text]
end
def accountable_fill_class(accountable_type)
class_mapping(accountable_type)[:fill]
end
def accountable_bg_class(accountable_type)
class_mapping(accountable_type)[:bg]
end
def accountable_bg_transparent_class(accountable_type)
class_mapping(accountable_type)[:bg_transparent]
end
def accountable_color(accountable_type)
class_mapping(accountable_type)[:hex]
end
def account_groups(period: nil)
assets, liabilities = Current.family.accounts.active.by_group(currency: Current.family.currency, period: period || Period.last_30_days).values_at(:assets, :liabilities)
[ assets.children.sort_by(&:name), liabilities.children.sort_by(&:name) ].flatten
end
private
def class_mapping(accountable_type)
{
"CreditCard" => { text: "text-red-500", bg: "bg-red-500", bg_transparent: "bg-red-500/10", fill: "fill-red-500", hex: "#F13636" },
"Loan" => { text: "text-fuchsia-500", bg: "bg-fuchsia-500", bg_transparent: "bg-fuchsia-500/10", fill: "fill-fuchsia-500", hex: "#D444F1" },
"OtherLiability" => { text: "text-gray-500", bg: "bg-gray-500", bg_transparent: "bg-gray-500/10", fill: "fill-gray-500", hex: "#737373" },
"Depository" => { text: "text-violet-500", bg: "bg-violet-500", bg_transparent: "bg-violet-500/10", fill: "fill-violet-500", hex: "#875BF7" },
"Investment" => { text: "text-blue-600", bg: "bg-blue-600", bg_transparent: "bg-blue-600/10", fill: "fill-blue-600", hex: "#1570EF" },
"OtherAsset" => { text: "text-green-500", bg: "bg-green-500", bg_transparent: "bg-green-500/10", fill: "fill-green-500", hex: "#12B76A" },
"Property" => { text: "text-cyan-500", bg: "bg-cyan-500", bg_transparent: "bg-cyan-500/10", fill: "fill-cyan-500", hex: "#06AED4" },
"Vehicle" => { text: "text-pink-500", bg: "bg-pink-500", bg_transparent: "bg-pink-500/10", fill: "fill-pink-500", hex: "#F23E94" }
}.fetch(accountable_type, { text: "text-gray-500", bg: "bg-gray-500", bg_transparent: "bg-gray-500/10", fill: "fill-gray-500", hex: "#737373" })
end
end

View File

@@ -19,6 +19,10 @@ module ApplicationHelper
content_for(:header_title) { page_title }
end
def header_description(page_description)
content_for(:header_description) { page_description }
end
def family_notifications_stream
turbo_stream_from [ Current.family, :notifications ] if Current.family
end
@@ -68,18 +72,8 @@ module ApplicationHelper
render partial: "shared/disclosure", locals: { title: title, content: content, open: default_open }
end
def sidebar_link_to(name, path, options = {})
is_current = current_page?(path) || (request.path.start_with?(path) && path != "/")
classes = [
"flex items-center gap-2 px-3 py-2 rounded-xl border text-sm font-medium text-gray-500",
(is_current ? "bg-white text-gray-900 shadow-xs border-alpha-black-50" : "hover:bg-gray-100 border-transparent")
].compact.join(" ")
link_to path, **options.merge(class: classes), aria: { current: ("page" if current_page?(path)) } do
concat(lucide_icon(options[:icon], class: "w-5 h-5")) if options[:icon]
concat(name)
end
def page_active?(path)
current_page?(path) || (request.path.start_with?(path) && path != "/")
end
def mixed_hex_styles(hex)
@@ -101,24 +95,6 @@ module ApplicationHelper
uri.relative? ? uri.path : root_path
end
def trend_styles(trend)
fallback = { bg_class: "bg-gray-500/5", text_class: "text-gray-500", symbol: "", icon: "minus" }
return fallback if trend.nil? || trend.direction.flat?
bg_class, text_class, symbol, icon = case trend.direction
when "up"
trend.favorable_direction.down? ? [ "bg-red-500/5", "text-red-500", "+", "arrow-up" ] : [ "bg-green-500/5", "text-green-500", "+", "arrow-up" ]
when "down"
trend.favorable_direction.down? ? [ "bg-green-500/5", "text-green-500", "-", "arrow-down" ] : [ "bg-red-500/5", "text-red-500", "-", "arrow-down" ]
when "flat"
[ "bg-gray-500/5", "text-gray-500", "", "minus" ]
else
raise ArgumentError, "Invalid trend direction: #{trend.direction}"
end
{ bg_class: bg_class, text_class: text_class, symbol: symbol, icon: icon }
end
# Wrapper around I18n.l to support custom date formats
def format_date(object, format = :default, options = {})
date = object.to_date
@@ -135,22 +111,12 @@ module ApplicationHelper
def format_money(number_or_money, options = {})
return nil unless number_or_money
money = Money.new(number_or_money)
options.reverse_merge!(money.format_options(I18n.locale))
number_to_currency(money.amount, options)
end
def format_money_without_symbol(number_or_money, options = {})
return nil unless number_or_money
money = Money.new(number_or_money)
options.reverse_merge!(money.format_options(I18n.locale))
ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] })
Money.new(number_or_money).format(options)
end
def totals_by_currency(collection:, money_method:, separator: " | ", negate: false)
collection.group_by(&:currency)
.transform_values { |item| negate ? item.sum(&money_method) * -1 : item.sum(&money_method) }
.transform_values { |item| calculate_total(item, money_method, negate) }
.map { |_currency, money| format_money(money) }
.join(separator)
end
@@ -162,4 +128,11 @@ module ApplicationHelper
cookies[:admin] == "true"
end
private
def calculate_total(item, money_method, negate)
items = item.reject { |i| i.respond_to?(:entryable) && i.entryable.transfer? }
total = items.sum(&money_method)
negate ? -total : total
end
end

View File

@@ -1,2 +0,0 @@
module EmailConfirmationsHelper
end

View File

@@ -11,25 +11,16 @@ module FormsHelper
end
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false)
form.label name, for: form.field_id(name, value), class: "group has-[:disabled]:cursor-not-allowed" do
form.label name, for: form.field_id(name, value), class: "group has-disabled:cursor-not-allowed" do
concat radio_tab_contents(label:, icon:)
concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
end
end
def period_select(form:, selected:, classes: "border border-alpha-black-100 shadow-xs rounded-lg text-sm pr-7 cursor-pointer text-gray-900 focus:outline-none focus:ring-0")
periods_for_select = [
%w[CWD current_week], # Current Week to Date
%w[7D last_7_days],
%w[MTD current_month], # Month to Date
%w[1M last_30_days],
%w[CQD current_quarter], # Quarter to Date
%w[3M last_90_days],
%w[YTD current_year], # Year to Date
%w[1Y last_365_days]
]
def period_select(form:, selected:, classes: "border border-secondary rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0")
periods_for_select = Period.all.map { |period| [ period.label_short, period.key ] }
form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" })
form.select(:period, periods_for_select, { selected: selected.key }, class: classes, data: { "auto-submit-form-target": "auto" })
end
@@ -39,9 +30,9 @@ end
private
def radio_tab_contents(label:, icon:)
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-gray-400 group-has-[:checked]:bg-white group-has-[:checked]:text-gray-800 group-has-[:checked]:shadow-sm") do
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued group-has-checked:bg-white group-has-checked:text-gray-800 group-has-checked:shadow-sm") do
concat lucide_icon(icon, class: "w-5 h-5")
concat tag.span(label, class: "group-has-[:checked]:font-semibold")
concat tag.span(label, class: "group-has-checked:font-semibold")
end
end
end

View File

@@ -1,2 +0,0 @@
module ImpersonationSessionsHelper
end

View File

@@ -20,6 +20,7 @@ module ImportsHelper
notes: "Notes",
qty: "Quantity",
ticker: "Ticker",
exchange: "Exchange",
price: "Price",
entity_type: "Type"
}[key]
@@ -45,7 +46,7 @@ module ImportsHelper
end
def cell_class(row, field)
base = "text-sm focus:ring-gray-900 focus:border-gray-900 w-full max-w-full disabled:text-gray-400"
base = "text-sm focus:ring-gray-900 focus:border-gray-900 w-full max-w-full disabled:text-subdued"
row.valid? # populate errors

View File

@@ -1,2 +0,0 @@
module InvitationsHelper
end

View File

@@ -7,8 +7,8 @@ module MenusHelper
end
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal)
link_to url, class: "flex items-center rounded-lg text-gray-900 hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-gray-500"))
link_to url, class: "flex items-center rounded-lg text-primary hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-secondary"))
concat(tag.span(label, class: "text-sm"))
end
end
@@ -26,7 +26,7 @@ module MenusHelper
private
def contextual_menu_icon
tag.button class: "flex hover:bg-gray-100 p-2 rounded cursor-pointer", data: { menu_target: "button" } do
lucide_icon "more-horizontal", class: "w-5 h-5 text-gray-500"
lucide_icon "more-horizontal", class: "w-5 h-5 text-secondary"
end
end

20
app/helpers/mfa_helper.rb Normal file
View File

@@ -0,0 +1,20 @@
module MfaHelper
def generate_mfa_qr_code(provisioning_uri)
qr_code = RQRCode::QRCode.new(provisioning_uri).as_svg(
color: "141414",
module_size: 4,
standalone: true,
use_path: true,
svg_attributes: {
width: "240",
height: "240",
viewBox: "0 0 65 65"
}
)
# Whitelist specific SVG attributes and elements that we know are safe
sanitize qr_code,
tags: %w[svg g path rect],
attributes: %w[viewBox height width fill stroke stroke-width d x y class]
end
end

View File

@@ -1,2 +0,0 @@
module PagesHelper
end

View File

@@ -0,0 +1,9 @@
module PlaidHelper
def plaid_webhooks_url(region = :us)
if Rails.env.production?
region.to_sym == :eu ? webhooks_plaid_eu_url : webhooks_plaid_url
else
ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/")) + "/webhooks/plaid#{region.to_sym == :eu ? '_eu' : ''}"
end
end
end

View File

@@ -1,2 +0,0 @@
module PropertiesHelper
end

View File

@@ -1,2 +0,0 @@
module SecuritiesHelper
end

View File

@@ -1,2 +0,0 @@
module Settings::BillingHelper
end

View File

@@ -1,2 +0,0 @@
module Settings::HostingHelper
end

Some files were not shown because too many files have changed in this diff Show More