diff --git a/app/models/tarif.rb b/app/models/tarif.rb index c7aa77b0d..a65ab0e82 100644 --- a/app/models/tarif.rb +++ b/app/models/tarif.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer @@ -151,20 +153,20 @@ def update_booking_conditions selecting_conditions.each { |condition| condition.assign_attributes(qualifiable: self, group: :selecting) } end - def minimum_prices(usage) + def minimum_prices(usage) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity nights = usage&.booking&.nights || 0 price_per_unit = usage&.price_per_unit || 0 { - minimum_usage_per_night: (minimum_usage_per_night || 0) * nights * price_per_unit, - minimum_usage_total: (minimum_usage_total || 0) * price_per_unit - # minimum_price_per_night: (minimum_price_per_night || 0) * nights, - # minimum_price_total: minimum_price_total || 0 + minimum_usage_per_night: (minimum_usage_per_night || 0) * price_per_unit * nights, + minimum_usage_total: (minimum_usage_total || 0) * price_per_unit, + minimum_price_per_night: (minimum_price_per_night || 0) * nights, + minimum_price_total: minimum_price_total || 0 } end def minimum_price(usage) - minimum_prices(usage).values.compact.max + minimum_prices(usage).filter { _2.positive? }.max_by { _2 } end def apply_usage_to_invoice?(_usage, _invoice) diff --git a/app/models/tarifs/amount.rb b/app/models/tarifs/amount.rb index 9b494e065..a8311d415 100644 --- a/app/models/tarifs/amount.rb +++ b/app/models/tarifs/amount.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer diff --git a/app/models/tarifs/flat.rb b/app/models/tarifs/flat.rb index dcd046198..51dcbd8c4 100644 --- a/app/models/tarifs/flat.rb +++ b/app/models/tarifs/flat.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer diff --git a/app/models/tarifs/group_minimum.rb b/app/models/tarifs/group_minimum.rb index a8e702588..269789561 100644 --- a/app/models/tarifs/group_minimum.rb +++ b/app/models/tarifs/group_minimum.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer @@ -52,8 +54,24 @@ def group_price(usage) usages_in_group(usage).sum(&:price) end + def group_used_units(usage) + usages_in_group(usage).sum(&:used_units) + end + + def minimum_prices_with_difference(usage) # rubocop:disable Metrics/AbcSize + nights = usage&.booking&.nights || 0 + price_per_unit = usage&.price_per_unit || 0 + minimum_prices = minimum_prices(usage) + { + minimum_usage_per_night: (((minimum_usage_per_night || 0) * nights) - group_used_units(usage)) * price_per_unit, + minimum_usage_total: ((minimum_usage_total || 0) - group_used_units(usage)) * price_per_unit, + minimum_price_per_night: minimum_prices[:minimum_price_per_night] - group_price(usage), + minimum_price_total: minimum_prices[:minimum_price_total] - group_price(usage) + } + end + def minimum_price(usage) - [minimum_prices(usage).values.max - group_price(usage), 0].max + minimum_prices_with_difference(usage).filter { _2.positive? }.max_by { _2 } end def apply_usage_to_invoice?(usage, _invoice) diff --git a/app/models/tarifs/metered.rb b/app/models/tarifs/metered.rb index 8163a50f5..5fcff9bae 100644 --- a/app/models/tarifs/metered.rb +++ b/app/models/tarifs/metered.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer diff --git a/app/models/tarifs/overnight_stay.rb b/app/models/tarifs/overnight_stay.rb index b78470b9f..6208be456 100644 --- a/app/models/tarifs/overnight_stay.rb +++ b/app/models/tarifs/overnight_stay.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer diff --git a/app/models/tarifs/price.rb b/app/models/tarifs/price.rb index c6d337bdc..3415633b1 100644 --- a/app/models/tarifs/price.rb +++ b/app/models/tarifs/price.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer diff --git a/app/models/usage.rb b/app/models/usage.rb index 0d0062a1f..cec610406 100644 --- a/app/models/usage.rb +++ b/app/models/usage.rb @@ -48,23 +48,19 @@ class Usage < ApplicationRecord validates :used_units, numericality: true, allow_nil: true def price - @price ||= ([usage_price, minimum_price].flatten.compact.max * 20.0).floor / 20.0 + @price ||= ([usage_price, minimum_price&.last].flatten.compact.max * 20.0).floor / 20.0 end def usage_price (used_units || 0.0) * (price_per_unit || 1.0) end - def minimum_prices - tarif&.minimum_prices(self) - end - def minimum_price tarif&.minimum_price(self) end def minimum_price? - price.positive? && price == minimum_price + price.positive? && price == minimum_price&.last end def presumed_units diff --git a/app/params/manage/tarif_params.rb b/app/params/manage/tarif_params.rb index d4dca5eba..fbdd8aa0b 100644 --- a/app/params/manage/tarif_params.rb +++ b/app/params/manage/tarif_params.rb @@ -4,8 +4,8 @@ module Manage class TarifParams < ApplicationParams def self.permitted_keys %i[type label unit price_per_unit ordinal tarif_group accountancy_account pin - prefill_usage_method prefill_usage_booking_question_id - minimum_usage_per_night minimum_usage_total vat] + + prefill_usage_method prefill_usage_booking_question_id vat + minimum_usage_per_night minimum_usage_total minimum_price_per_night minimum_price_total] + I18n.available_locales.map { |locale| ["label_#{locale}", "unit_#{locale}"] }.flatten + [{ associated_types: [], selecting_conditions_attributes: BookingConditionParams.permitted_keys + %i[id _destroy], diff --git a/app/views/manage/tarifs/_form.html.slim b/app/views/manage/tarifs/_form.html.slim index 17a4a5726..e4ae5c4e9 100644 --- a/app/views/manage/tarifs/_form.html.slim +++ b/app/views/manage/tarifs/_form.html.slim @@ -39,6 +39,8 @@ span.fa.fa-thermometer.text-danger.ms-2 = f.text_field :minimum_usage_per_night, step: 0.01, inputmode: "numeric" = f.text_field :minimum_usage_total, step: 0.01, inputmode: "numeric" + = f.text_field :minimum_price_per_night, step: 0.01, inputmode: "numeric" + = f.text_field :minimum_price_total, step: 0.01, inputmode: "numeric" fieldset = f.collection_check_boxes :associated_types, Tarif.associated_types.keys, :itself, ->(key) { Tarif::ASSOCIATED_TYPES[key]&.model_name&.human } diff --git a/app/views/manage/tarifs/_table.html.slim b/app/views/manage/tarifs/_table.html.slim index e581e7325..a3dad351b 100644 --- a/app/views/manage/tarifs/_table.html.slim +++ b/app/views/manage/tarifs/_table.html.slim @@ -7,7 +7,6 @@ th th th= Tarif.human_attribute_name(:label) - th= Tarif.human_attribute_name(:type) th= Tarif.human_attribute_name(:price_per_unit) th= Tarif.human_attribute_name(:unit) th @@ -35,12 +34,11 @@ = tf.hidden_field :id td p.mb-0 = tarif.label - td - = tf.select :type, subtype_options_for_select(Tarif.subtypes), { skip_label: true, wrapper: false }, { disabled: true } td = tf.text_field :price_per_unit, skip_label: true, wrapper: false, disabled: tarif.discarded?, inputmode: "numeric" td - = tf.static_control :unit, skip_label: true, wrapper: false + = tarif.unit + small.d-block.text-secondary= tarif.class.model_name.human td.text-end .btn-group - unless tarif.discarded? diff --git a/app/views/renderables/tarifs/group_minimum/_usage_form.html.slim b/app/views/renderables/tarifs/group_minimum/_usage_form.html.slim index 2c0238c0f..0a05c84e0 100644 --- a/app/views/renderables/tarifs/group_minimum/_usage_form.html.slim +++ b/app/views/renderables/tarifs/group_minimum/_usage_form.html.slim @@ -1,13 +1,82 @@ = render layout: 'manage/tarifs/usage_form', locals: { usage:, f: } - - f.object.valid? + - usage.valid? # trigger #before_validate .d-flex.gap-3.justify-content-end - div - = number_to_currency(f.object.tarif.minimum_prices(f.object).values.max, unit: '') - small.d-block.text-secondary= @booking.organisation.currency + - case usage.minimum_price&.first + - when :minimum_usage_per_night + div + | ( - div - | - + div + = number_with_delimiter(usage.tarif.minimum_usage_per_night * usage.booking.nights) + + div + | - + + div + = number_with_delimiter(usage.tarif.group_used_units(usage)) + + div + | ) + + div + | × + + div + = number_to_currency(usage.price_per_unit, unit: '') + small.d-block.text-secondary= @booking.organisation.currency + + div + | = + + - when :minimum_usage_total + div + | ( + + div + = number_with_delimiter(usage.tarif.minimum_usage_total) + + div + | - + + div + = number_with_delimiter(usage.tarif.group_used_units(usage)) + + div + | ) + + div + | × + + div + = number_to_currency(usage.price_per_unit, unit: '') + small.d-block.text-secondary= @booking.organisation.currency + + div + | = + + - when :minimum_price_per_night + div + = number_to_currency(usage.tarif.minimum_price_per_night * usage.booking.nights) + + div + | - + + div + = number_to_currency(usage.tarif.group_price(usage)) + + div + | = + + - when :minimum_price_total + div + = number_to_currency(usage.tarif.minimum_price_total) + + div + | - + + div + = number_to_currency(usage.tarif.group_price(usage)) + + div + | = - div - = number_to_currency(f.object.tarif.group_price(f.object), unit: '') - small.d-block.text-secondary= @booking.organisation.currency diff --git a/config/locales/de.yml b/config/locales/de.yml index ceb7c2449..8a79ef182 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -333,6 +333,8 @@ de: enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung meter: Name des Zählers (bei Tarifen nach Zählerstand) + minimum_price_per_night: Minimaler Preis (pro Nacht) + minimum_price_total: Minimaler Preis (Total) minimum_usage_per_night: Minimaler Verbrauch (pro Nacht) minimum_usage_total: Minimaler Verbrauch (Total) ordinal_position: Ordnungsnummer diff --git a/db/migrate/20240930094421_add_minimum_price_to_tarifs.rb b/db/migrate/20240930094421_add_minimum_price_to_tarifs.rb new file mode 100644 index 000000000..49a6a47d6 --- /dev/null +++ b/db/migrate/20240930094421_add_minimum_price_to_tarifs.rb @@ -0,0 +1,6 @@ +class AddMinimumPriceToTarifs < ActiveRecord::Migration[7.2] + def change + add_column :tarifs, :minimum_price_per_night, :decimal, null: true + add_column :tarifs, :minimum_price_total, :decimal, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 6fe725b35..593f93f5a 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_09_23_110730) do +ActiveRecord::Schema[7.2].define(version: 2024_09_30_094421) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -540,6 +540,8 @@ t.datetime "discarded_at" t.decimal "vat" t.bigint "prefill_usage_booking_question_id" + t.decimal "minimum_price_per_night" + t.decimal "minimum_price_total" t.index ["discarded_at"], name: "index_tarifs_on_discarded_at" t.index ["organisation_id"], name: "index_tarifs_on_organisation_id" t.index ["prefill_usage_booking_question_id"], name: "index_tarifs_on_prefill_usage_booking_question_id" diff --git a/spec/factories/tarifs.rb b/spec/factories/tarifs.rb index 427d11c9f..ad8ccb11a 100644 --- a/spec/factories/tarifs.rb +++ b/spec/factories/tarifs.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer @@ -40,8 +42,9 @@ # FactoryBot.define do - factory :tarif, class: Tarifs::Amount.to_s do - # type Tarifs::Amount + factory :tarif do + type { Tarifs::Amount.sti_name } + initialize_with { type.constantize.new } label { 'Preis pro Übernachtung' } unit { 'Übernachtung (unter 16 Jahren)' } price_per_unit { 15.0 } diff --git a/spec/models/tarif_spec.rb b/spec/models/tarif_spec.rb index e2ad6de36..aa72db44e 100644 --- a/spec/models/tarif_spec.rb +++ b/spec/models/tarif_spec.rb @@ -9,6 +9,8 @@ # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) # minimum_usage_per_night :decimal(, ) # minimum_usage_total :decimal(, ) # ordinal :integer @@ -41,7 +43,7 @@ require 'rails_helper' -RSpec.describe Organisation, type: :model do +RSpec.describe Tarif, type: :model do let(:home) { create(:home) } let(:organisation) { home.organisation } let(:booking) { create(:booking, organisation:, home:) } @@ -56,19 +58,42 @@ subject(:price) { usage.price } it { is_expected.to eq(70.0) } - # it { expect(tarif.minimum_price?(usage)).to be_falsy } + end + + describe '#minimum_prices' do + subject(:minimum_prices) { usage.tarif.minimum_prices(usage) } + before do + tarif.update({ + minimum_usage_per_night: 24, + minimum_usage_total: 71, + minimum_price_per_night: 210, + minimum_price_total: 610 + }) + end + + it 'lists all minimum prices' do + expect(minimum_prices).to eq({ + minimum_usage_per_night: 24 * 7 * 10, + minimum_usage_total: 71 * 10, + minimum_price_per_night: 210 * 7, + minimum_price_total: 610 + }) + end + end - # context 'with minimum_price_total' do - # before { tarif.update(minimum_price_total: 100.0) } - # it { is_expected.to eq(100.0) } - # it { expect(tarif.minimum_price?(usage)).to be_truthy } - # end + describe '#minimum_price' do + subject(:minimum_price) { usage.tarif.minimum_price(usage) } + before do + tarif.update({ + minimum_usage_per_night: 24, + minimum_usage_total: 71, + minimum_price_per_night: 210, + minimum_price_total: 610 + }) + end - # context 'with minimum_price_per_night' do - # before { tarif.update(minimum_price_per_night: 100.0) } - # it { expect(booking.nights).to eq(7) } - # it { is_expected.to eq(700.0) } - # it { expect(tarif.minimum_price?(usage)).to be_truthy } - # end + it 'lists all minimum prices' do + expect(minimum_price).to eq([:minimum_usage_per_night, 24 * 7 * 10]) + end end end diff --git a/spec/models/tarifs/group_minimum_spec.rb b/spec/models/tarifs/group_minimum_spec.rb new file mode 100644 index 000000000..638fc6379 --- /dev/null +++ b/spec/models/tarifs/group_minimum_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: tarifs +# +# id :bigint not null, primary key +# accountancy_account :string +# associated_types :integer default(0), not null +# discarded_at :datetime +# label_i18n :jsonb +# minimum_price_per_night :decimal(, ) +# minimum_price_total :decimal(, ) +# minimum_usage_per_night :decimal(, ) +# minimum_usage_total :decimal(, ) +# ordinal :integer +# pin :boolean default(TRUE) +# prefill_usage_method :string +# price_per_unit :decimal(, ) +# tarif_group :string +# type :string +# unit_i18n :jsonb +# valid_from :datetime +# valid_until :datetime +# vat :decimal(, ) +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# prefill_usage_booking_question_id :bigint +# +# Indexes +# +# index_tarifs_on_discarded_at (discarded_at) +# index_tarifs_on_organisation_id (organisation_id) +# index_tarifs_on_prefill_usage_booking_question_id (prefill_usage_booking_question_id) +# index_tarifs_on_type (type) +# +# Foreign Keys +# +# fk_rails_... (organisation_id => organisations.id) +# fk_rails_... (prefill_usage_booking_question_id => booking_questions.id) +# + +require 'rails_helper' + +RSpec.describe Tarifs::GroupMinimum, type: :model do + let(:home) { create(:home) } + let(:organisation) { home.organisation } + let(:booking) { create(:booking, organisation:, home:) } + + let(:tarif_group) { :test } + let(:price_per_unit) { 10 } + let(:tarif) { create(:tarif, type: described_class.sti_name, price_per_unit:, organisation:, tarif_group:) } + let(:other_tarif1) { create(:tarif, price_per_unit:, organisation:, tarif_group:) } + let(:other_tarif2) { create(:tarif, price_per_unit: 12, organisation:, tarif_group:) } + let!(:usage) { create(:usage, booking:, tarif:) } + let!(:other_usage1) { create(:usage, booking:, tarif: other_tarif1, used_units: 7) } + let!(:other_usage2) { create(:usage, booking:, tarif: other_tarif2, used_units: 8) } + + describe '#minimum_prices' do + subject(:minimum_prices) { tarif.minimum_prices_with_difference(usage) } + before do + tarif.update({ + minimum_usage_per_night: 24, + minimum_usage_total: 71, + minimum_price_per_night: 210, + minimum_price_total: 610 + }) + end + + it 'lists all minimum prices' do + expect(tarif.group_price(usage)).to eq((7 * 10) + (8 * 12)) + expect(tarif.group_used_units(usage)).to eq(7 + 8) + expect(minimum_prices).to eq({ + minimum_usage_per_night: ((24 * 7) - 15) * 10, + minimum_usage_total: ((71 - 15) * 10), + minimum_price_per_night: (210 * 7) - ((7 * 10) + (8 * 12)), + minimum_price_total: 610 - ((7 * 10) + (8 * 12)) + }) + end + end + + describe '#minimum_price' do + subject(:minimum_price) { usage.tarif.minimum_price(usage) } + before do + tarif.update({ + minimum_usage_per_night: 24, + minimum_usage_total: 71, + minimum_price_per_night: 210, + minimum_price_total: 610 + }) + end + + it 'lists all minimum prices' do + expect(minimum_price.first).to eq(:minimum_usage_per_night) + end + end +end