diff --git a/app/controllers/properties_controller.rb b/app/controllers/properties_controller.rb new file mode 100644 index 00000000..2219594b --- /dev/null +++ b/app/controllers/properties_controller.rb @@ -0,0 +1,34 @@ +class PropertiesController < ApplicationController + before_action :set_account, only: :update + + def create + account = Current.family.accounts.create!(account_params) + account.sync_later + redirect_to account, notice: t(".success") + end + + def update + @account.update!(account_params) + @account.sync_later + redirect_to @account, notice: t(".success") + end + + private + + def set_account + @account = Current.family.accounts.find(params[:id]) + end + + def account_params + params.require(:account) + .permit( + :name, :balance, :currency, :accountable_type, + accountable_attributes: [ + :id, + :year_built, + :area, + address_attributes: [ :line1, :line2, :locality, :region, :country, :postal_code ] + ] + ) + end +end diff --git a/app/helpers/properties_helper.rb b/app/helpers/properties_helper.rb new file mode 100644 index 00000000..e9841903 --- /dev/null +++ b/app/helpers/properties_helper.rb @@ -0,0 +1,2 @@ +module PropertiesHelper +end diff --git a/app/models/account.rb b/app/models/account.rb index 9f17f756..bd2f28cf 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -28,6 +28,8 @@ class Account < ApplicationRecord delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy + accepts_nested_attributes_for :accountable + delegate :value, :series, to: :accountable class << self diff --git a/app/models/address.rb b/app/models/address.rb new file mode 100644 index 00000000..1ab76cda --- /dev/null +++ b/app/models/address.rb @@ -0,0 +1,26 @@ +class Address < ApplicationRecord + belongs_to :addressable, polymorphic: true + + validates :line1, :locality, presence: true + validates :region, presence: true, format: { with: /\A[A-Z]{1,3}(-[A-Z\d]{1,3})?\z/, message: "must be a valid ISO3166-2 code" } + validates :country, presence: true, format: { with: /\A[A-Z]{2}\z/, message: "must be a valid ISO3166-1 Alpha-2 code" } + validates :postal_code, presence: true, if: :postal_code_required? + + def to_s + I18n.t("address.format", + line1: line1, + line2: line2, + county: county, + locality: locality, + region: region, + country: country, + postal_code: postal_code + ) + end + + private + + def postal_code_required? + country.in?(%w[US CA GB]) + end +end diff --git a/app/models/measurement.rb b/app/models/measurement.rb new file mode 100644 index 00000000..56b3ec43 --- /dev/null +++ b/app/models/measurement.rb @@ -0,0 +1,20 @@ +class Measurement + include ActiveModel::Validations + + attr_reader :value, :unit + + VALID_UNITS = %w[sqft sqm] + + validates :unit, inclusion: { in: VALID_UNITS } + validates :value, presence: true + + def initialize(value, unit) + @value = value.to_f + @unit = unit.to_s.downcase.strip + validate! + end + + def to_s + "#{@value.to_i} #{@unit}" + end +end diff --git a/app/models/property.rb b/app/models/property.rb index cae953c9..f6638ac9 100644 --- a/app/models/property.rb +++ b/app/models/property.rb @@ -1,3 +1,11 @@ class Property < ApplicationRecord include Accountable + + has_one :address, as: :addressable, dependent: :destroy + + accepts_nested_attributes_for :address + + def area + Measurement.new(area_value, area_unit) + end end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 7e58f1f3..028f9607 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -29,3 +29,4 @@ ignore_unused: - 'helpers.submit.*' # i18n-tasks does not detect used at forms - 'helpers.label.*' # i18n-tasks does not detect used at forms - 'accounts.show.sync_message_*' # messages generated in the sync ActiveJob + - 'address.attributes.*' diff --git a/config/locales/models/address/en.yml b/config/locales/models/address/en.yml new file mode 100644 index 00000000..eb0db551 --- /dev/null +++ b/config/locales/models/address/en.yml @@ -0,0 +1,15 @@ +--- +en: + address: + attributes: + country: Country + line1: Address Line 1 + line2: Address Line 2 + locality: Locality + postal_code: Postal Code + region: Region + format: |- + %{line1} + %{line2} + %{locality}, %{region} %{postal_code} + %{country} diff --git a/config/locales/views/properties/en.yml b/config/locales/views/properties/en.yml new file mode 100644 index 00000000..a59f2f24 --- /dev/null +++ b/config/locales/views/properties/en.yml @@ -0,0 +1,7 @@ +--- +en: + properties: + create: + success: Property created successfully + update: + success: Property updated successfully diff --git a/config/routes.rb b/config/routes.rb index bbf8c8ac..25b92740 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,6 +89,8 @@ Rails.application.routes.draw do end end + resources :properties, only: %i[ create update ] + resources :transactions, only: %i[ index new create ] do collection do post "bulk_delete" diff --git a/db/migrate/20240822174006_create_addresses.rb b/db/migrate/20240822174006_create_addresses.rb new file mode 100644 index 00000000..a4e7e1bd --- /dev/null +++ b/db/migrate/20240822174006_create_addresses.rb @@ -0,0 +1,16 @@ +class CreateAddresses < ActiveRecord::Migration[7.2] + def change + create_table :addresses, id: :uuid do |t| + t.references :addressable, type: :uuid, polymorphic: true + t.string :line1 + t.string :line2 + t.string :county + t.string :locality + t.string :region + t.string :country + t.integer :postal_code + + t.timestamps + end + end +end diff --git a/db/migrate/20240822180845_add_property_attributes.rb b/db/migrate/20240822180845_add_property_attributes.rb new file mode 100644 index 00000000..793a0fa8 --- /dev/null +++ b/db/migrate/20240822180845_add_property_attributes.rb @@ -0,0 +1,7 @@ +class AddPropertyAttributes < ActiveRecord::Migration[7.2] + def change + add_column :properties, :year_built, :integer + add_column :properties, :area_value, :integer + add_column :properties, :area_unit, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a423928..cb2fe1ff 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_08_17_144454) do +ActiveRecord::Schema[7.2].define(version: 2024_08_22_180845) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -152,6 +152,21 @@ ActiveRecord::Schema[7.2].define(version: 2024_08_17_144454) do t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "addresses", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "addressable_type" + t.uuid "addressable_id" + t.string "line1" + t.string "line2" + t.string "county" + t.string "locality" + t.string "region" + t.string "country" + t.integer "postal_code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["addressable_type", "addressable_id"], name: "index_addresses_on_addressable" + end + create_table "categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", null: false t.string "color", default: "#6172F3", null: false @@ -358,6 +373,9 @@ ActiveRecord::Schema[7.2].define(version: 2024_08_17_144454) do create_table "properties", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "year_built" + t.integer "area_value" + t.string "area_unit" end create_table "securities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/test/controllers/properties_controller_test.rb b/test/controllers/properties_controller_test.rb new file mode 100644 index 00000000..7b34d9b6 --- /dev/null +++ b/test/controllers/properties_controller_test.rb @@ -0,0 +1,71 @@ +require "test_helper" + +class PropertiesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in @user = users(:family_admin) + @account = accounts(:property) + end + + test "creates property" do + assert_difference [ "Account.count", "Property.count" ] do + post properties_path, params: { + account: { + name: "Property", + balance: 500000, + currency: "USD", + accountable_type: "Property", + accountable_attributes: { + year_built: 2002, + area_value: 1000, + area_unit: "sqft", + address_attributes: { + line1: "123 Main St", + line2: "Apt 1", + locality: "Los Angeles", + region: "CA", # ISO3166-2 code + country: "US", # ISO3166-1 Alpha-2 code + postal_code: "90001" + } + } + } + } + end + + created_account = Account.order(:created_at).last + + assert_redirected_to account_path(created_account) + assert_equal "Property created successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end + + test "updates property" do + assert_no_difference [ "Account.count", "Property.count" ] do + patch property_path(@account), params: { + account: { + name: "Updated Property", + balance: 500000, + currency: "USD", + accountable_type: "Property", + accountable_attributes: { + id: @account.accountable_id, + year_built: 2002, + area_value: 1000, + area_unit: "sqft", + address_attributes: { + line1: "123 Main St", + line2: "Apt 1", + locality: "Los Angeles", + region: "CA", # ISO3166-2 code + country: "US", # ISO3166-1 Alpha-2 code + postal_code: "90001" + } + } + } + } + end + + assert_redirected_to account_path(@account) + assert_equal "Property updated successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end +end diff --git a/test/fixtures/addresses.yml b/test/fixtures/addresses.yml new file mode 100644 index 00000000..0124887e --- /dev/null +++ b/test/fixtures/addresses.yml @@ -0,0 +1,9 @@ +one: + line1: 123 Main Street + line2: Apt 4B + locality: Los Angeles + region: CA + country: US + postal_code: 90001 + addressable: one + addressable_type: Property diff --git a/test/fixtures/properties.yml b/test/fixtures/properties.yml index e0553ab0..c330c7f3 100644 --- a/test/fixtures/properties.yml +++ b/test/fixtures/properties.yml @@ -1 +1,4 @@ -one: { } \ No newline at end of file +one: + year_built: 2002 + area_value: 1000 + area_unit: "sqft" \ No newline at end of file diff --git a/test/models/address_test.rb b/test/models/address_test.rb new file mode 100644 index 00000000..e6d50548 --- /dev/null +++ b/test/models/address_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class AddressTest < ActiveSupport::TestCase + test "can print a formatted address" do + address = Address.new( + line1: "123 Main St", + locality: "San Francisco", + region: "CA", + country: "US", + postal_code: "94101" + ) + + assert_equal "123 Main St\n\nSan Francisco, CA 94101\nUS", address.to_s + end +end