1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-21 22:29:38 +02:00

Scaffold out basic transactions model and UI (#478)

* Transaction scaffold

* Rough in transaction views

* Fix sort order

* Fix mass assignment issue

* Fix test

* Simplify CI workflow

* Don't seed db before test
This commit is contained in:
Zach Gollwitzer 2024-02-23 21:34:33 -05:00 committed by GitHub
parent e767aca37f
commit 87b97b3c41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 430 additions and 44 deletions

View file

@ -85,18 +85,16 @@ jobs:
ruby-version: .ruby-version ruby-version: .ruby-version
bundler-cache: true bundler-cache: true
- name: Run tests - name: Run tests and smoke test seed
env: env:
RAILS_ENV: test RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432 DATABASE_URL: postgres://postgres:postgres@localhost:5432
# REDIS_URL: redis://localhost:6379/0 # REDIS_URL: redis://localhost:6379/0
run: bin/rails db:setup test test:system run: |
bin/rails db:create
- name: Smoke test database seeds bin/rails db:schema:load
env: bin/rails test:all
RAILS_ENV: test bin/rails db:seed
DATABASE_URL: postgres://postgres:postgres@localhost:5432
run: bin/rails db:drop db:create db:migrate db:seed
- name: Keep screenshots from failed system tests - name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -0,0 +1,64 @@
class TransactionsController < ApplicationController
before_action :authenticate_user!
before_action :set_transaction, only: %i[ show edit update destroy ]
def index
@transactions = Current.family.transactions
end
def show
end
def new
@transaction = Transaction.new
end
def edit
end
def create
@transaction = Transaction.new(transaction_params)
account = Current.family.accounts.find(params[:transaction][:account_id])
raise ActiveRecord::RecordNotFound, "Account not found or not accessible" if account.nil?
@transaction.account = account
respond_to do |format|
if @transaction.save
format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @transaction.update(transaction_params)
format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully updated." }
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
def destroy
@transaction.destroy!
respond_to do |format|
format.html { redirect_to transactions_url, notice: "Transaction was successfully destroyed." }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_transaction
@transaction = Transaction.find(params[:id])
end
# Only allow a list of trusted parameters through.
def transaction_params
params.require(:transaction).permit(:name, :date, :amount, :currency)
end
end

View file

@ -34,6 +34,18 @@ class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
end end
end end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
default_options = { class: "form-field__input" }
merged_options = default_options.merge(html_options)
return super(method, collection, value_method, text_method, options, merged_options) unless options[:label]
@template.form_field_tag do
label(method, *label_args(options)) +
super(method, collection, value_method, text_method, options, merged_options.except(:label))
end
end
def submit(value = nil, options = {}) def submit(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash) value, options = nil, value if value.is_a?(Hash)
default_options = { class: "form-field__submit" } default_options = { class: "form-field__submit" }

View file

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

View file

@ -0,0 +1,33 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="tabs"
export default class extends Controller {
static classes = ["active"];
static targets = ["btn", "tab"];
static values = { defaultTab: String };
connect() {
const defaultTab = this.defaultTabValue;
this.tabTargets.forEach((tab) => {
if (tab.id === defaultTab) {
tab.hidden = false;
this.btnTargets
.find((btn) => btn.dataset.id === defaultTab)
.classList.add(...this.activeClasses);
} else {
tab.hidden = true;
}
});
}
select(event) {
const selectedTabId = event.currentTarget.dataset.id;
this.tabTargets.forEach((tab) => (tab.hidden = tab.id !== selectedTabId));
this.btnTargets.forEach((btn) =>
btn.classList.toggle(
...this.activeClasses,
btn.dataset.id === selectedTabId
)
);
}
}

View file

@ -3,6 +3,7 @@ class Account < ApplicationRecord
belongs_to :family belongs_to :family
has_many :balances, class_name: "AccountBalance" has_many :balances, class_name: "AccountBalance"
has_many :valuations has_many :valuations
has_many :transactions
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy

View file

@ -1,4 +1,5 @@
class Family < ApplicationRecord class Family < ApplicationRecord
has_many :users, dependent: :destroy has_many :users, dependent: :destroy
has_many :accounts, dependent: :destroy has_many :accounts, dependent: :destroy
has_many :transactions, through: :accounts
end end

View file

@ -0,0 +1,3 @@
class Transaction < ApplicationRecord
belongs_to :account
end

View file

@ -0,0 +1,29 @@
<%# locals: (account:, valuation_series:) %>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="flex justify-between items-center">
<h3 class="font-medium text-lg">History</h3>
<%= link_to new_account_valuation_path(account), data: { turbo_frame: dom_id(Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
<span class="text-sm">New entry</span>
<% end %>
</div>
<div class="rounded-xl bg-gray-25 p-1">
<div class="flex flex-col rounded-lg space-y-1">
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
<div class="w-16">date</div>
<div class="flex items-center justify-between grow">
<div></div>
<div>value</div>
</div>
<div class="w-56 text-right">change</div>
<div class="w-[72px]"></div>
</div>
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
<%= turbo_frame_tag dom_id(Valuation.new) %>
<%= turbo_frame_tag "valuations_list" do %>
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuation_series } %>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,19 @@
<%# locals: (transactions:)%>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="flex justify-between items-center">
<h3 class="font-medium text-lg">Transactions</h3>
<%= link_to new_transaction_path, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
<span class="text-sm">New transaction</span>
<% end %>
</div>
<% if transactions.empty? %>
<p class="text-gray-500 py-4">No transactions for this account yet.</p>
<% else %>
<div class="space-y-6">
<% transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
<% end %>
</div>
<% end %>
</div>

View file

@ -59,31 +59,17 @@
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs"> <div data-controller="tabs" data-tabs-active-class="bg-gray-100" data-tabs-default-tab-value="account-history-tab">
<div class="flex justify-between items-center"> <div class="flex gap-1 text-sm text-gray-900 font-medium mb-4">
<h3 class="font-medium text-lg">History</h3> <button data-id="account-history-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">History</button>
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: dom_id(Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %> <button data-id="account-transactions-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Transactions</button>
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
<span class="text-sm">New entry</span>
<% end %>
</div> </div>
<div class="rounded-xl bg-gray-25 p-1"> <div class="min-h-[800px]">
<div class="flex flex-col rounded-lg space-y-1"> <div data-tabs-target="tab" id="account-history-tab">
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2"> <%= render partial: "accounts/account_history", locals: { account: @account, valuation_series: @valuation_series } %>
<div class="w-16">date</div>
<div class="flex items-center justify-between grow">
<div></div>
<div>value</div>
</div>
<div class="w-56 text-right">change</div>
<div class="w-[72px]"></div>
</div>
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
<%= turbo_frame_tag dom_id(Valuation.new) %>
<%= turbo_frame_tag "valuations_list" do %>
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: @valuation_series } %>
<% end %>
</div> </div>
<div data-tabs-target="tab" id="account-transactions-tab">
<%= render partial: "accounts/transactions", locals: { transactions: @account.transactions.order(date: :desc) } %>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,10 +6,8 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Maybe"> <meta name="apple-mobile-web-app-title" content="Maybe">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icon.png" type="image/png"> <link rel="icon" href="/icon.png" type="image/png">
<link rel="icon" href="/icon.svg" type="image/svg+xml"> <link rel="icon" href="/icon.svg" type="image/svg+xml">
@ -18,15 +16,12 @@
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
<%= hotwire_livereload_tags if Rails.env.development? %> <%= hotwire_livereload_tags if Rails.env.development? %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %> <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= yield :head %> <%= yield :head %>
</head> </head>
<body class="h-full"> <body class="h-full">
<div id="notification-tray" class="fixed top-6 right-6 space-y-1 z-50"></div> <div id="notification-tray" class="fixed top-6 right-6 space-y-1 z-50"></div>
<%= safe_join(flash.map { |type, message| notification(message, type: type) }) %> <%= safe_join(flash.map { |type, message| notification(message, type: type) }) %>
<div class="flex"> <div class="flex">
<div class="flex-col p-5 min-w-80"> <div class="flex-col p-5 min-w-80">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -37,14 +32,12 @@
<div class="flex" data-action="click->dropdown#toggleMenu"> <div class="flex" data-action="click->dropdown#toggleMenu">
<div class="mr-1.5 text-white w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div> <div class="mr-1.5 text-white w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
</div> </div>
<div class="absolute z-10 hidden w-screen px-2 mt-2 -translate-x-1/2 left-1/2 max-w-min" data-dropdown-target="menu"> <div class="absolute z-10 hidden w-screen px-2 mt-2 -translate-x-1/2 left-1/2 max-w-min" data-dropdown-target="menu">
<div class="w-48 px-3 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5"> <div class="w-48 px-3 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to "Settings", edit_settings_path, class: 'block p-2 hover:text-gray-600' %> <%= link_to "Settings", edit_settings_path, class: 'block p-2 hover:text-gray-600' %>
<%= button_to "Log Out", session_path, method: :delete, class: 'block p-2 hover:text-gray-600' %> <%= button_to "Log Out", session_path, method: :delete, class: 'block p-2 hover:text-gray-600' %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<nav> <nav>
@ -56,7 +49,7 @@
<%= sidebar_link_to t('.accounts'), accounts_path, icon: 'layers' %> <%= sidebar_link_to t('.accounts'), accounts_path, icon: 'layers' %>
</li> </li>
<li> <li>
<%= sidebar_link_to t('.transactions'), "#", icon: 'credit-card' %> <%= sidebar_link_to t('.transactions'), transactions_path, icon: 'credit-card' %>
</li> </li>
</ul> </ul>
</nav> </nav>
@ -69,13 +62,10 @@
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %> <%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
<% end %> <% end %>
</div> </div>
<%= link_to new_account_path, class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %> <%= link_to new_account_path, class: "flex items-center gap-4 px-2 py-3 mb-1 text-gray-500 text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %> <%= lucide_icon("plus", class: "w-5 h-5") %>
<p><%= t('.new_account') %></p> <p><%= t('.new_account') %></p>
<% end %> <% end %>
<% Accountable.types.each do |type| %> <% Accountable.types.each do |type| %>
<%= render 'accounts/account_list', type: Accountable.from_type(type) %> <%= render 'accounts/account_list', type: Accountable.from_type(type) %>
<% end %> <% end %>

View file

@ -0,0 +1,7 @@
<%= form_with model: @transaction do |f| %>
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account" } %>
<%= f.date_field :date, label: "Date" %>
<%= f.text_field :name, label: "Name" %>
<%= f.number_field :amount, label: "Amount", step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00 %>
<%= f.submit %>
<% end %>

View file

@ -0,0 +1,12 @@
<div id="<%= dom_id transaction %>" class="flex items-center gap-6 py-4 text-sm font-medium">
<div class="w-96 flex items-center gap-2">
<div class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-600/5 text-gray-600"><%= transaction.name[0].upcase %></div>
<p class="text-gray-900"><%= transaction.name %></p>
</div>
<div>
<p><%= transaction.account.name %></p>
</div>
<div class="ml-auto">
<p class="<%= transaction.amount < 0 ? "text-green-600" : "" %>"><%= number_to_currency(-transaction.amount, { precision: 2 }) %></p>
</div>
</div>

View file

@ -0,0 +1,10 @@
<%# locals: (date:, transactions:) %>
<div class="bg-gray-25 rounded-xl p-1">
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-gray-500">
<h4><%= date.strftime('%b %d, %Y') %> &middot; <%= transactions.size %></h4>
<span><%= number_to_currency(-transactions.sum(&:amount)) %></span>
</div>
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50 px-4">
<%= render partial: "transactions/transaction", collection: transactions %>
</div>
</div>

View file

@ -0,0 +1,8 @@
<div class="mx-auto md:w-2/3 w-full">
<h1 class="font-bold text-4xl">Editing transaction</h1>
<%= render "form", transaction: @transaction %>
<%= link_to "Show this transaction", @transaction, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to transactions", transactions_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
</div>

View file

@ -0,0 +1,75 @@
<div class="space-y-4">
<div class="flex justify-between items-center text-gray-900 font-medium">
<h1 class="text-xl">Transactions</h1>
<div class="flex items-center gap-5">
<div class="flex items-center gap-1 cursor-not-allowed">
<span class="text-sm">USD $</span>
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
</div>
<div class="border-r border-alpha-black-200 h-5"></div>
<div class="flex items-center gap-2">
<%= lucide_icon("settings-2", class: "cursor-not-allowed w-5 h-5 text-gray-500") %>
<%= link_to new_transaction_path, class: "rounded-full w-9 h-9 bg-gray-900 text-white flex items-center justify-center hover:bg-gray-700" do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<% end %>
</div>
</div>
</div>
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs px-4 divide-x divide-alpha-black-100">
<div class="p-4 space-y-2">
<p class="text-sm text-gray-500">Total transactions</p>
<p class="text-gray-900 font-medium text-xl"><%= @transactions.size %></p>
</div>
<div class="p-4 space-y-2">
<p class="text-sm text-gray-500">Income</p>
<p class="text-gray-900 font-medium text-xl">
<%= number_to_currency(@transactions.select { |t| t.amount < 0 }.sum(&:amount).abs, precision: 2) %>
</p>
</div>
<div class="p-4 space-y-2">
<p class="text-sm text-gray-500">Expenses</p>
<p class="text-gray-900 font-medium text-xl">
<%= number_to_currency(@transactions.select { |t| t.amount >= 0 }.sum(&:amount), precision: 2) %>
</p>
</div>
</div>
<div id="transactions" class="bg-white rounded-xl border border-alpha-black-25 shadow-xs p-4 space-y-4">
<div class="flex gap-2">
<div class="grow cursor-not-allowed">
<%= form_with url: transactions_path, method: :get, local: true, html: { role: 'search' } do |form| %>
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
<%= form.text_field :search, placeholder: "Search transaction by merchant, category or amount", class: "placeholder:text-sm placeholder:text-gray-500 relative pl-10 w-full border-none rounded-lg cursor-not-allowed", 'data-action': "input->search#perform", disabled: true %>
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
</div>
<% end %>
</div>
<div>
<button class="cursor-not-allowed border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
<p class="text-sm font-medium text-gray-900">Filter</p>
</button>
</div>
<%= form_with url: "#", method: :get, class: "flex items-center gap-4", html: { class: "" } do |f| %>
<%= f.select :period, options_for_select([['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']], selected: params[:period]), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2 cursor-not-allowed", onchange: "this.form.submit();", disabled: true } %>
<% end %>
</div>
<div class="bg-gray-25 rounded-xl px-5 py-3 text-xs font-medium text-gray-500 flex items-center gap-6">
<div class="w-96">
<p class="uppercase">transaction</p>
</div>
<div class="grow uppercase flex justify-between items-center gap-5 text-xs font-medium text-gray-500">
<p>account</p>
<p>amount</p>
</div>
</div>
<% if @transactions.empty? %>
<p class="text-gray-500 py-4">No transactions for this account yet.</p>
<% else %>
<div class="space-y-6">
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %>
<%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %>
<% end %>
</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,7 @@
<div class="mx-auto md:w-2/3 w-full">
<h1 class="font-bold text-4xl mb-4">New transaction</h1>
<%= render "form", transaction: @transaction %>
</div>
<div class="flex justify-center">
<%= link_to "Back to transactions", transactions_path, class: "mt-8 underline text-lg font-bold" %>
</div>

View file

@ -0,0 +1,10 @@
<div class="mx-auto md:w-2/3 w-full flex">
<div class="mx-auto">
<%= render @transaction %>
<%= link_to "Edit this transaction", edit_transaction_path(@transaction), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<div class="inline-block ml-2">
<%= button_to "Destroy this transaction", transaction_path(@transaction), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
</div>
<%= link_to "Back to transactions", transactions_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
</div>
</div>

View file

@ -7,6 +7,7 @@ Rails.application.routes.draw do
resource :password resource :password
resource :settings, only: %i[edit update] resource :settings, only: %i[edit update]
resources :transactions
resources :accounts, shallow: true do resources :accounts, shallow: true do
resources :valuations resources :valuations
end end

View file

@ -0,0 +1,13 @@
class CreateTransactions < ActiveRecord::Migration[7.2]
def change
create_table :transactions, id: :uuid do |t|
t.string :name
t.date :date, null: false
t.decimal :amount, precision: 19, scale: 4, null: false
t.string :currency, default: "USD", null: false
t.references :account, null: false, type: :uuid, foreign_key: { on_delete: :cascade }
t.timestamps
end
end
end

14
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_02_22_144849) do ActiveRecord::Schema[7.2].define(version: 2024_02_23_162105) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -196,6 +196,17 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_22_144849) do
t.index ["token"], name: "index_invite_codes_on_token", unique: true t.index ["token"], name: "index_invite_codes_on_token", unique: true
end end
create_table "transactions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name"
t.date "date", null: false
t.decimal "amount", precision: 19, scale: 4, null: false
t.string "currency", default: "USD", null: false
t.uuid "account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_transactions_on_account_id"
end
create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "family_id", null: false t.uuid "family_id", null: false
t.string "first_name" t.string "first_name"
@ -220,6 +231,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_22_144849) do
add_foreign_key "account_balances", "accounts", on_delete: :cascade add_foreign_key "account_balances", "accounts", on_delete: :cascade
add_foreign_key "accounts", "families" add_foreign_key "accounts", "families"
add_foreign_key "transactions", "accounts", on_delete: :cascade
add_foreign_key "users", "families" add_foreign_key "users", "families"
add_foreign_key "valuations", "accounts", on_delete: :cascade add_foreign_key "valuations", "accounts", on_delete: :cascade
end end

View file

@ -40,6 +40,15 @@ valuations = [
{ date: Date.today - 3, value: 301900 } { date: Date.today - 3, value: 301900 }
] ]
transactions = [
{ date: Date.today - 27, amount: 7.56, currency: "USD", name: "Starbucks" },
{ date: Date.today - 18, amount: -2000, currency: "USD", name: "Paycheck" },
{ date: Date.today - 18, amount: 18.20, currency: "USD", name: "Walgreens" },
{ date: Date.today - 13, amount: 34.20, currency: "USD", name: "Chipotle" },
{ date: Date.today - 9, amount: -200, currency: "USD", name: "Birthday check" },
{ date: Date.today - 5, amount: 85.00, currency: "USD", name: "Amazon stuff" }
]
# Represent system-generated "Balances" at various dates, based on valuations # Represent system-generated "Balances" at various dates, based on valuations
balances = [ balances = [
{ date: Date.today - 30, balance: 300000 }, { date: Date.today - 30, balance: 300000 },
@ -75,6 +84,8 @@ balances = [
{ date: Date.today, balance: current_balance } { date: Date.today, balance: current_balance }
] ]
valuations.each do |valuation| valuations.each do |valuation|
Valuation.find_or_create_by( Valuation.find_or_create_by(
account_id: account.id, account_id: account.id,
@ -93,3 +104,14 @@ balances.each do |balance|
balance_record.balance = balance[:balance] balance_record.balance = balance[:balance]
end end
end end
transactions.each do |transaction|
Transaction.find_or_create_by(
account_id: account.id,
date: transaction[:date],
amount: transaction[:amount]
) do |transaction_record|
transaction_record.currency = transaction[:currency]
transaction_record.name = transaction[:name]
end
end

View file

@ -0,0 +1,49 @@
require "test_helper"
class TransactionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:bob)
@transaction = transactions(:one)
end
test "should get index" do
get transactions_url
assert_response :success
end
test "should get new" do
get new_transaction_url
assert_response :success
end
test "should create transaction" do
assert_difference("Transaction.count") do
post transactions_url, params: { transaction: { account_id: @transaction.account_id, amount: @transaction.amount, currency: @transaction.currency, date: @transaction.date, name: @transaction.name } }
end
assert_redirected_to transaction_url(Transaction.last)
end
test "should show transaction" do
get transaction_url(@transaction)
assert_response :success
end
test "should get edit" do
get edit_transaction_url(@transaction)
assert_response :success
end
test "should update transaction" do
patch transaction_url(@transaction), params: { transaction: { account_id: @transaction.account_id, amount: @transaction.amount, currency: @transaction.currency, date: @transaction.date, name: @transaction.name } }
assert_redirected_to transaction_url(@transaction)
end
test "should destroy transaction" do
assert_difference("Transaction.count", -1) do
delete transaction_url(@transaction)
end
assert_redirected_to transactions_url
end
end

15
test/fixtures/transactions.yml vendored Normal file
View file

@ -0,0 +1,15 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
date: 2024-02-23
amount: 9.99
currency: MyString
account: dylan_checking
two:
name: MyString
date: 2024-02-20
amount: 9.99
currency: MyString
account: dylan_checking

View file

@ -0,0 +1,7 @@
require "test_helper"
class TransactionTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end