Compare commits
21 Commits
missing-se
...
1615-pre-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf014bc24f | ||
|
|
90f1ff8a0b | ||
|
|
b84a33c09d | ||
|
|
abd932c894 | ||
|
|
5b083c9e33 | ||
|
|
f498212b2d | ||
|
|
37aab45c19 | ||
|
|
c9c5eb315a | ||
|
|
75c8627577 | ||
|
|
15e8281d46 | ||
|
|
283d9cd8e2 | ||
|
|
3e06017ae1 | ||
|
|
058830591f | ||
|
|
32d826c047 | ||
|
|
bdec61f312 | ||
|
|
9c846e7de4 | ||
|
|
4c158934d0 | ||
|
|
50e5ffb257 | ||
|
|
983729cbdf | ||
|
|
89027f1fbf | ||
|
|
2a338eb01b |
@@ -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 user’s 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 todo’s, 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.
|
||||
78
.cursor/rules/project-conventions.mdc
Normal file
78
.cursor/rules/project-conventions.mdc
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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 possible, models should answer questions about themselves—for example, we might have a method, `account.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)
|
||||
|
||||
134
.cursor/rules/project-design.mdc
Normal file
134
.cursor/rules/project-design.mdc
Normal 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.
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'Bug: '
|
||||
title: 'Bug: [Add descriptive title here]'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
5
Gemfile
5
Gemfile
@@ -21,7 +21,9 @@ 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"
|
||||
@@ -31,6 +33,7 @@ gem "stackprof"
|
||||
gem "rack-mini-profiler"
|
||||
gem "sentry-ruby"
|
||||
gem "sentry-rails"
|
||||
gem "logtail-rails"
|
||||
|
||||
# Active Storage
|
||||
gem "aws-sdk-s3", "~> 1.177.0", require: false
|
||||
|
||||
32
Gemfile.lock
32
Gemfile.lock
@@ -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
|
||||
@@ -194,10 +205,6 @@ 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)
|
||||
@@ -243,6 +250,17 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
logger (1.6.5)
|
||||
logtail (0.1.13)
|
||||
msgpack (~> 1.0)
|
||||
logtail-rack (0.2.5)
|
||||
logtail (~> 0.1)
|
||||
rack (>= 1.2, < 4.0)
|
||||
logtail-rails (0.2.9)
|
||||
actionpack (>= 5.0.0)
|
||||
activerecord (>= 5.0.0)
|
||||
logtail (~> 0.1)
|
||||
logtail-rack (~> 0.1)
|
||||
railties (>= 5.0.0)
|
||||
loofah (2.24.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
@@ -300,6 +318,9 @@ GEM
|
||||
plaid (35.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)
|
||||
@@ -519,7 +540,7 @@ DEPENDENCIES
|
||||
faraday-retry
|
||||
good_job
|
||||
hotwire-livereload
|
||||
hotwire_combobox
|
||||
hotwire_combobox!
|
||||
i18n-tasks
|
||||
image_processing (>= 1.2)
|
||||
importmap-rails
|
||||
@@ -527,6 +548,7 @@ DEPENDENCIES
|
||||
intercom-rails
|
||||
jwt
|
||||
letter_opener
|
||||
logtail-rails
|
||||
lucide-rails!
|
||||
mocha
|
||||
octokit
|
||||
|
||||
18
README.md
18
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
|
||||
if params[:id] == BudgetCategory.uncategorized.id
|
||||
@@ -23,13 +23,25 @@ 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
|
||||
@budget = Current.family.budgets.find(params[:budget_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
handleInput = (event) => {
|
||||
const target = event.target
|
||||
const target = event.target;
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
@@ -34,18 +34,18 @@ export default class extends Controller {
|
||||
};
|
||||
|
||||
#debounceTimeout(element) {
|
||||
if(element.dataset.autosubmitDebounceTimeout) {
|
||||
if (element.dataset.autosubmitDebounceTimeout) {
|
||||
return Number.parseInt(element.dataset.autosubmitDebounceTimeout);
|
||||
}
|
||||
|
||||
const type = element.type || element.tagName;
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case 'input':
|
||||
case 'textarea':
|
||||
case "input":
|
||||
case "textarea":
|
||||
return 500;
|
||||
case 'select-one':
|
||||
case 'select-multiple':
|
||||
case "select-one":
|
||||
case "select-multiple":
|
||||
return 0;
|
||||
default:
|
||||
return 500;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class EnrichDataJob < ApplicationJob
|
||||
queue_as :latency_high
|
||||
|
||||
def perform(account)
|
||||
account.enrich_data
|
||||
end
|
||||
end
|
||||
8
app/jobs/enrich_transaction_batch_job.rb
Normal file
8
app/jobs/enrich_transaction_batch_job.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class EnrichTransactionBatchJob < ApplicationJob
|
||||
queue_as :latency_high
|
||||
|
||||
def perform(account, batch_size = 100, offset = 0)
|
||||
enricher = Account::DataEnricher.new(account)
|
||||
enricher.enrich_transaction_batch(batch_size, offset)
|
||||
end
|
||||
end
|
||||
@@ -130,10 +130,6 @@ class Account < ApplicationRecord
|
||||
DataEnricher.new(self).run
|
||||
end
|
||||
|
||||
def enrich_data_later
|
||||
EnrichDataJob.perform_later(self)
|
||||
end
|
||||
|
||||
def update_with_sync!(attributes)
|
||||
should_update_balance = attributes[:balance] && attributes[:balance].to_d != balance
|
||||
|
||||
|
||||
@@ -8,49 +8,61 @@ class Account::DataEnricher
|
||||
end
|
||||
|
||||
def run
|
||||
enrich_transactions
|
||||
end
|
||||
total_unenriched = account.entries.account_transactions
|
||||
.joins("JOIN account_transactions at ON at.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
|
||||
.where("account_entries.enriched_at IS NULL OR at.merchant_id IS NULL OR at.category_id IS NULL")
|
||||
.count
|
||||
|
||||
private
|
||||
def enrich_transactions
|
||||
candidates = account.entries.account_transactions.includes(entryable: [ :merchant, :category ])
|
||||
if total_unenriched > 0
|
||||
batch_size = 50
|
||||
batches = (total_unenriched.to_f / batch_size).ceil
|
||||
|
||||
Rails.logger.info("Enriching #{candidates.count} transactions for account #{account.id}")
|
||||
|
||||
merchants = {}
|
||||
|
||||
candidates.each do |entry|
|
||||
if entry.enriched_at.nil? || entry.entryable.merchant_id.nil? || entry.entryable.category_id.nil?
|
||||
begin
|
||||
next unless entry.name.present?
|
||||
|
||||
info = self.class.synth_provider.enrich_transaction(entry.name).info
|
||||
|
||||
next unless info.present?
|
||||
|
||||
if info.name.present?
|
||||
merchant = merchants[info.name] ||= account.family.merchants.find_or_create_by(name: info.name)
|
||||
|
||||
if info.icon_url.present?
|
||||
merchant.icon_url = info.icon_url
|
||||
end
|
||||
end
|
||||
|
||||
entryable_attributes = { id: entry.entryable_id }
|
||||
entryable_attributes[:merchant_id] = merchant.id if merchant.present? && entry.entryable.merchant_id.nil?
|
||||
|
||||
Account.transaction do
|
||||
merchant.save! if merchant.present?
|
||||
entry.update!(
|
||||
enriched_at: Time.current,
|
||||
enriched_name: info.name,
|
||||
entryable_attributes: entryable_attributes
|
||||
)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.warn("Error enriching transaction #{entry.id}: #{e.message}")
|
||||
end
|
||||
end
|
||||
batches.times do |batch|
|
||||
EnrichTransactionBatchJob.perform_later(account, batch_size, batch * batch_size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def enrich_transaction_batch(batch_size = 50, offset = 0)
|
||||
candidates = account.entries.account_transactions
|
||||
.includes(entryable: [ :merchant, :category ])
|
||||
.joins("JOIN account_transactions at ON at.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
|
||||
.where("account_entries.enriched_at IS NULL OR at.merchant_id IS NULL OR at.category_id IS NULL")
|
||||
.offset(offset)
|
||||
.limit(batch_size)
|
||||
|
||||
Rails.logger.info("Enriching batch of #{candidates.count} transactions for account #{account.id} (offset: #{offset})")
|
||||
|
||||
merchants = {}
|
||||
|
||||
candidates.each do |entry|
|
||||
begin
|
||||
info = self.class.synth_provider.enrich_transaction(entry.name).info
|
||||
|
||||
next unless info.present?
|
||||
|
||||
if info.name.present?
|
||||
merchant = merchants[info.name] ||= account.family.merchants.find_or_create_by(name: info.name)
|
||||
|
||||
if info.icon_url.present?
|
||||
merchant.icon_url = info.icon_url
|
||||
end
|
||||
end
|
||||
|
||||
entryable_attributes = { id: entry.entryable_id }
|
||||
entryable_attributes[:merchant_id] = merchant.id if merchant.present? && entry.entryable.merchant_id.nil?
|
||||
|
||||
Account.transaction do
|
||||
merchant.save! if merchant.present?
|
||||
entry.update!(
|
||||
enriched_at: Time.current,
|
||||
enriched_name: info.name,
|
||||
entryable_attributes: entryable_attributes
|
||||
)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.warn("Error enriching transaction #{entry.id}: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,11 +46,22 @@ class Account::HoldingCalculator
|
||||
end
|
||||
|
||||
def generate_holding_records(portfolio, date)
|
||||
Rails.logger.info "[HoldingCalculator] Generating holdings for #{portfolio.size} securities on #{date}"
|
||||
|
||||
portfolio.map do |security_id, qty|
|
||||
security = securities_cache[security_id]
|
||||
|
||||
if security.blank?
|
||||
Rails.logger.error "[HoldingCalculator] Security #{security_id} not found in cache for account #{account.id}"
|
||||
next
|
||||
end
|
||||
|
||||
price = security.dig(:prices)&.find { |p| p.date == date }
|
||||
|
||||
next if price.blank?
|
||||
if price.blank?
|
||||
Rails.logger.info "[HoldingCalculator] No price found for security #{security_id} on #{date}"
|
||||
next
|
||||
end
|
||||
|
||||
converted_price = Money.new(price.price, price.currency).exchange_to(account.currency, fallback_rate: 1).amount
|
||||
|
||||
@@ -106,19 +117,35 @@ class Account::HoldingCalculator
|
||||
end
|
||||
|
||||
def preload_securities
|
||||
# Get securities from trades and current holdings
|
||||
securities = trades.map(&:entryable).map(&:security).uniq
|
||||
securities += account.holdings.where(date: Date.current).map(&:security)
|
||||
securities.uniq!
|
||||
|
||||
Rails.logger.info "[HoldingCalculator] Preloading #{securities.size} securities for account #{account.id}"
|
||||
|
||||
securities.each do |security|
|
||||
prices = Security::Price.find_prices(
|
||||
security: security,
|
||||
start_date: portfolio_start_date,
|
||||
end_date: Date.current
|
||||
)
|
||||
begin
|
||||
Rails.logger.info "[HoldingCalculator] Loading security: ID=#{security.id} Ticker=#{security.ticker}"
|
||||
|
||||
@securities_cache[security.id] = {
|
||||
security: security,
|
||||
prices: prices
|
||||
}
|
||||
prices = Security::Price.find_prices(
|
||||
security: security,
|
||||
start_date: portfolio_start_date,
|
||||
end_date: Date.current
|
||||
)
|
||||
|
||||
Rails.logger.info "[HoldingCalculator] Found #{prices.size} prices for security #{security.id}"
|
||||
|
||||
@securities_cache[security.id] = {
|
||||
security: security,
|
||||
prices: prices
|
||||
}
|
||||
rescue => e
|
||||
Rails.logger.error "[HoldingCalculator] Error processing security #{security.id}: #{e.message}"
|
||||
Rails.logger.error "[HoldingCalculator] Security details: #{security.attributes}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
next # Skip this security and continue with others
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class Account::Syncer
|
||||
|
||||
# Enrich if user opted in or if we're syncing transactions from a Plaid account on the hosted app
|
||||
if account.family.data_enrichment_enabled? || (account.plaid_account_id.present? && Rails.application.config.app_mode.hosted?)
|
||||
account.enrich_data_later
|
||||
account.enrich_data
|
||||
else
|
||||
Rails.logger.info("Data enrichment is disabled, skipping enrichment for account #{account.id}")
|
||||
end
|
||||
|
||||
@@ -37,16 +37,12 @@ class Family < ApplicationRecord
|
||||
update!(last_synced_at: Time.current)
|
||||
|
||||
accounts.manual.each do |account|
|
||||
account.sync_data(start_date: start_date)
|
||||
account.sync_later(start_date: start_date)
|
||||
end
|
||||
|
||||
plaid_data = []
|
||||
|
||||
plaid_items.each do |plaid_item|
|
||||
plaid_data << plaid_item.sync_data(start_date: start_date)
|
||||
plaid_item.sync_later(start_date: start_date)
|
||||
end
|
||||
|
||||
plaid_data
|
||||
end
|
||||
|
||||
def post_sync
|
||||
|
||||
@@ -41,7 +41,7 @@ class PlaidItem < ApplicationRecord
|
||||
plaid_data = fetch_and_load_plaid_data
|
||||
|
||||
accounts.each do |account|
|
||||
account.sync_data(start_date: start_date)
|
||||
account.sync_later(start_date: start_date)
|
||||
end
|
||||
|
||||
plaid_data
|
||||
|
||||
@@ -180,6 +180,8 @@ class Provider::Plaid
|
||||
end
|
||||
|
||||
def get_primary_product(accountable_type)
|
||||
return "transactions" if eu?
|
||||
|
||||
case accountable_type
|
||||
when "Investment"
|
||||
"investments"
|
||||
@@ -191,11 +193,17 @@ class Provider::Plaid
|
||||
end
|
||||
|
||||
def get_additional_consented_products(accountable_type)
|
||||
return [] if eu?
|
||||
|
||||
MAYBE_SUPPORTED_PLAID_PRODUCTS - [ get_primary_product(accountable_type) ]
|
||||
end
|
||||
|
||||
def eu?
|
||||
region.to_sym == :eu
|
||||
end
|
||||
|
||||
def country_codes
|
||||
if region.to_sym == :eu
|
||||
if eu?
|
||||
[ "ES", "NL", "FR", "IE", "DE", "IT", "PL", "DK", "NO", "SE", "EE", "LT", "LV", "PT", "BE" ] # EU supported countries
|
||||
else
|
||||
[ "US", "CA" ] # US + CA only
|
||||
|
||||
@@ -59,7 +59,7 @@ class Provider::Synth
|
||||
{
|
||||
date: price.dig("date"),
|
||||
price: price.dig("close")&.to_f || price.dig("open")&.to_f,
|
||||
currency: price.dig("currency") || "USD"
|
||||
currency: body.dig("currency") || "USD"
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -133,6 +133,7 @@ class Provider::Synth
|
||||
req.params["name"] = query
|
||||
req.params["dataset"] = dataset
|
||||
req.params["country_code"] = country_code
|
||||
req.params["limit"] = 25
|
||||
end
|
||||
|
||||
parsed = JSON.parse(response.body)
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
<%# locals: (budget:) %>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<div id="<%= dom_id(budget, :allocation_progress) %>" class="space-y-2 mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="rounded-full w-1.5 h-1.5 <%= budget.allocated_spending > 0 ? "bg-gray-900" : "bg-gray-100" %>"></div>
|
||||
<% if budget.available_to_allocate.negative? %>
|
||||
<div class="rounded-full w-1.5 h-1.5 bg-red-500"></div>
|
||||
<% else %>
|
||||
<div class="rounded-full w-1.5 h-1.5 <%= budget.allocated_spending > 0 ? "bg-gray-900" : "bg-gray-100" %>"></div>
|
||||
<% end %>
|
||||
|
||||
<p class="text-gray-500 text-sm">
|
||||
<%= number_to_percentage(budget.allocated_percent, precision: 0) %> set
|
||||
</p>
|
||||
<% if budget.available_to_allocate.negative? %>
|
||||
<p class="text-gray-900 text-sm">> 100% set</p>
|
||||
<% else %>
|
||||
<p class="text-gray-500 text-sm">
|
||||
<%= number_to_percentage(budget.allocated_percent, precision: 0) %> set
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p class="ml-auto text-sm space-x-1">
|
||||
<span class="text-gray-900"><%= format_money(budget.allocated_spending_money) %></span>
|
||||
<span class="<%= budget.available_to_allocate.negative? ? "text-red-500" : "text-gray-900" %>"><%= format_money(budget.allocated_spending_money) %></span>
|
||||
<span class="text-gray-500"> / </span>
|
||||
<span class="text-gray-500"><%= format_money(budget.budgeted_spending_money) %></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative h-1.5 rounded-2xl bg-gray-100">
|
||||
<div class="absolute inset-0 bg-gray-900 rounded-2xl" style="width: <%= budget.allocated_percent %>%;"></div>
|
||||
<% if budget.available_to_allocate.negative? %>
|
||||
<div class="absolute inset-0 bg-red-500 rounded-2xl" style="width: 100%;"></div>
|
||||
<% else %>
|
||||
<div class="absolute inset-0 bg-gray-900 rounded-2xl" style="width: <%= budget.allocated_percent %>%;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-900"><%= format_money(budget.available_to_allocate_money) %></span>
|
||||
<span class="text-gray-500">left to allocate</span>
|
||||
<% if budget.available_to_allocate.negative? %>
|
||||
<p class="text-gray-500">
|
||||
Budget exceeded by <span class="text-red-500"><%= format_money(budget.available_to_allocate_money.abs) %></span>
|
||||
</p>
|
||||
<% else %>
|
||||
<span class="text-gray-900"><%= format_money(budget.available_to_allocate_money) %></span>
|
||||
<span class="text-gray-500">left to allocate</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<%# locals: (budget:) %>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="rounded-full w-1.5 h-1.5 bg-red-500"></div>
|
||||
|
||||
<p class="text-gray-900 text-sm">> 100% set</p>
|
||||
|
||||
<p class="ml-auto text-sm space-x-1">
|
||||
<span class="text-red-500"><%= format_money(budget.allocated_spending_money) %></span>
|
||||
<span class="text-gray-500"> / </span>
|
||||
<span class="text-gray-500"><%= format_money(budget.budgeted_spending_money) %></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative h-1.5 rounded-2xl bg-gray-100">
|
||||
<div class="absolute inset-0 bg-red-500 rounded-2xl" style="width: 100%;"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<p class="text-gray-500">
|
||||
Budget exceeded by <span class="text-red-500"><%= format_money(budget.available_to_allocate_money.abs) %></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<%= form_with model: [budget_category.budget, budget_category], data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "blur", turbo_frame: :_top } do |f| %>
|
||||
<%= form_with model: [budget_category.budget, budget_category], data: { controller: "auto-submit-form preserve-focus" } do |f| %>
|
||||
<div class="form-field w-[120px]">
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-500 text-sm mr-2"><%= currency.symbol %></span>
|
||||
@@ -20,6 +20,7 @@
|
||||
class: "form-field__input text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
|
||||
placeholder: "0",
|
||||
step: currency.step,
|
||||
id: dom_id(budget_category, :budgeted_spending),
|
||||
min: 0,
|
||||
data: { auto_submit_form_target: "auto" } %>
|
||||
</div>
|
||||
|
||||
@@ -21,14 +21,10 @@
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="max-w-md mx-auto">
|
||||
<% if @budget.available_to_allocate.negative? %>
|
||||
<%= render "budget_categories/allocation_progress_overage", budget: @budget %>
|
||||
<% else %>
|
||||
<%= render "budget_categories/allocation_progress", budget: @budget %>
|
||||
<% end %>
|
||||
<%= render "budget_categories/allocation_progress", budget: @budget %>
|
||||
|
||||
<div class="space-y-4 mb-4">
|
||||
<% BudgetCategory::Group.for(@budget.budget_categories).sort_by(&:name).each do |group| %>
|
||||
<% BudgetCategory::Group.for(@budget_categories).sort_by(&:name).each do |group| %>
|
||||
<div class="space-y-4">
|
||||
<%= render "budget_categories/budget_category_form", budget_category: group.budget_category %>
|
||||
|
||||
|
||||
1
app/views/budget_categories/update.turbo_stream.erb
Normal file
1
app/views/budget_categories/update.turbo_stream.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= turbo_stream.replace dom_id(@budget, :allocation_progress), partial: "budget_categories/allocation_progress", locals: { budget: @budget } %>
|
||||
@@ -1,8 +1,8 @@
|
||||
default: &default
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
# 3 connections for Puma, 8 for GoodJob (in async mode, the default for self-hosters) = 11 connections
|
||||
pool: <%= ENV.fetch("DB_POOL_SIZE") { 11 } %>
|
||||
# 3 connections for Puma, 15 for GoodJob (in async mode, the default for self-hosters) = 18 connections
|
||||
pool: <%= ENV.fetch("DB_POOL_SIZE") { 18 } %>
|
||||
host: <%= ENV.fetch("DB_HOST") { "127.0.0.1" } %>
|
||||
port: <%= ENV.fetch("DB_PORT") { "5432" } %>
|
||||
user: <%= ENV.fetch("POSTGRES_USER") { nil } %>
|
||||
|
||||
@@ -48,10 +48,17 @@ Rails.application.configure do
|
||||
# Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
|
||||
config.assume_ssl = ActiveModel::Type::Boolean.new.cast(ENV.fetch("RAILS_ASSUME_SSL", true))
|
||||
|
||||
# Log to STDOUT by default
|
||||
config.logger = ActiveSupport::Logger.new(STDOUT)
|
||||
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
|
||||
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
|
||||
# Log to Logtail if API key is present, otherwise log to STDOUT
|
||||
config.logger = if ENV["LOGTAIL_API_KEY"].present?
|
||||
Logtail::Logger.create_default_logger(
|
||||
ENV["LOGTAIL_API_KEY"],
|
||||
telemetry_host: "in.logs.betterstack.com"
|
||||
)
|
||||
else
|
||||
ActiveSupport::Logger.new(STDOUT)
|
||||
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
|
||||
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
|
||||
end
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
config.log_tags = [ :request_id ]
|
||||
|
||||
@@ -13,13 +13,11 @@ Rails.application.configure do
|
||||
|
||||
config.good_job.on_thread_error = ->(exception) { Rails.error.report(exception) }
|
||||
|
||||
# 5 queue threads + 3 for job listener, cron, executor = 8 threads allocated
|
||||
config.queues = {
|
||||
"latency_low" => { max_threads: 1, priority: 10 }, # ~30s jobs
|
||||
"latency_low,latency_medium" => { max_threads: 2, priority: 5 }, # ~1-2 min jobs
|
||||
"latency_low,latency_medium,latency_high" => { max_threads: 1, priority: 1 }, # ~5+ min jobs
|
||||
"*" => { max_threads: 1, priority: 0 } # fallback queue
|
||||
}
|
||||
# 7 dedicated queue threads + 5 catch-all threads + 3 for job listener, cron, executor = 15 threads allocated
|
||||
# `latency_low` queue for jobs ~30s
|
||||
# `latency_medium` queue for jobs ~1-2 min
|
||||
# `latency_high` queue for jobs ~5+ min
|
||||
config.good_job.queues = "latency_low:2;latency_low,latency_medium:3;latency_low,latency_medium,latency_high:2;*"
|
||||
|
||||
# Auth for jobs admin dashboard
|
||||
ActiveSupport.on_load(:good_job_application_controller) do
|
||||
|
||||
@@ -8,11 +8,11 @@ if ENV["SENTRY_DSN"].present?
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for performance monitoring.
|
||||
# We recommend adjusting this value in production.
|
||||
config.traces_sample_rate = 1.0
|
||||
config.traces_sample_rate = 0.5
|
||||
|
||||
# Set profiles_sample_rate to profile 100%
|
||||
# of sampled transactions.
|
||||
# We recommend adjusting this value in production.
|
||||
config.profiles_sample_rate = 1.0
|
||||
config.profiles_sample_rate = 0.5
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,11 +16,11 @@ class FamilyTest < ActiveSupport::TestCase
|
||||
manual_accounts_count = @syncable.accounts.manual.count
|
||||
items_count = @syncable.plaid_items.count
|
||||
|
||||
Account.any_instance.expects(:sync_data)
|
||||
Account.any_instance.expects(:sync_later)
|
||||
.with(start_date: nil)
|
||||
.times(manual_accounts_count)
|
||||
|
||||
PlaidItem.any_instance.expects(:sync_data)
|
||||
PlaidItem.any_instance.expects(:sync_later)
|
||||
.with(start_date: nil)
|
||||
.times(items_count)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user