diff --git a/serverside_challenge_2/challenge/.rspec b/serverside_challenge_2/challenge/.rspec new file mode 100644 index 000000000..c99d2e739 --- /dev/null +++ b/serverside_challenge_2/challenge/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/serverside_challenge_2/challenge/.rubocop.yml b/serverside_challenge_2/challenge/.rubocop.yml new file mode 100644 index 000000000..db1c23f63 --- /dev/null +++ b/serverside_challenge_2/challenge/.rubocop.yml @@ -0,0 +1,10 @@ +Documentation: + Enabled: false +MethodLength: + CountComments: true + Max: 25 + Style/FrozenStringLiteralComment: + Enabled: false +Metrics: + Enabled: false + \ No newline at end of file diff --git a/serverside_challenge_2/challenge/Gemfile b/serverside_challenge_2/challenge/Gemfile index 43bf67fe3..a12bf9c7d 100644 --- a/serverside_challenge_2/challenge/Gemfile +++ b/serverside_challenge_2/challenge/Gemfile @@ -36,9 +36,12 @@ gem "bootsnap", require: false # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem "rack-cors" +gem 'rubocop-rails', '~> 2.32' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mingw x64_mingw ] + gem 'rspec-rails' end group :development do diff --git a/serverside_challenge_2/challenge/Gemfile.lock b/serverside_challenge_2/challenge/Gemfile.lock index a47fb85f5..d6148432f 100644 --- a/serverside_challenge_2/challenge/Gemfile.lock +++ b/serverside_challenge_2/challenge/Gemfile.lock @@ -66,6 +66,7 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) + ast (2.4.3) bootsnap (1.18.3) msgpack (~> 1.2) builder (3.2.4) @@ -75,6 +76,7 @@ GEM debug (1.9.1) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.6.2) erubi (1.12.0) globalid (1.2.1) activesupport (>= 6.1) @@ -84,6 +86,9 @@ GEM irb (1.11.2) rdoc reline (>= 0.4.2) + json (2.13.0) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -111,7 +116,12 @@ GEM racc (~> 1.4) nokogiri (1.16.2-x86_64-linux) racc (~> 1.4) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc pg (1.5.4) + prism (1.4.0) psych (5.1.2) stringio puma (5.6.8) @@ -148,16 +158,59 @@ GEM rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.1.0) rdoc (6.6.2) psych (>= 4.0.0) + regexp_parser (2.10.0) reline (0.4.2) io-console (~> 0.5) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.1.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.4) + rubocop (1.78.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-rails (2.32.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) stringio (3.1.0) thor (1.3.0) timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -173,6 +226,8 @@ DEPENDENCIES pg (~> 1.1) puma (~> 5.0) rails (~> 7.0.8) + rspec-rails + rubocop-rails (~> 2.32) tzinfo-data RUBY VERSION diff --git a/serverside_challenge_2/challenge/app/controllers/api/v1/power_supply_plan_controller.rb b/serverside_challenge_2/challenge/app/controllers/api/v1/power_supply_plan_controller.rb new file mode 100644 index 000000000..294345c2c --- /dev/null +++ b/serverside_challenge_2/challenge/app/controllers/api/v1/power_supply_plan_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Api + module V1 + class PowerSupplyPlanController < ApplicationController + def simulate_all + simulator = PowerSupplyPlanSimulator.new(params[:amp], params[:meter_rate]) + result = simulator.call + + return render json: [result] if result.is_a?(Hash) && result.key?(:error) + + render json: result + end + end + end +end diff --git a/serverside_challenge_2/challenge/app/models/basic_charge.rb b/serverside_challenge_2/challenge/app/models/basic_charge.rb new file mode 100644 index 000000000..a7c0f9e58 --- /dev/null +++ b/serverside_challenge_2/challenge/app/models/basic_charge.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BasicCharge < ApplicationRecord + belongs_to :power_supply_plan +end diff --git a/serverside_challenge_2/challenge/app/models/meter_rate_charge.rb b/serverside_challenge_2/challenge/app/models/meter_rate_charge.rb new file mode 100644 index 000000000..2b932e845 --- /dev/null +++ b/serverside_challenge_2/challenge/app/models/meter_rate_charge.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class MeterRateCharge < ApplicationRecord + belongs_to :power_supply_plan + + def calculate_charge(meter_rate) + min_rate = min_meter_rate + max_rate = max_meter_rate + step_price = price + return 0 if min_rate > meter_rate + + if max_rate.nil? + return step_price * meter_rate if min_rate.zero? + + return step_price * (meter_rate - min_rate + 1) + end + if max_rate.to_i < meter_rate.to_i + return step_price * (max_rate - min_rate) if min_rate.zero? + + step_price * (max_rate - min_rate + 1) + else + return step_price * (meter_rate - min_rate) if min_rate.zero? + + step_price * (meter_rate - min_rate + 1) + end + end +end diff --git a/serverside_challenge_2/challenge/app/models/power_supply_plan.rb b/serverside_challenge_2/challenge/app/models/power_supply_plan.rb new file mode 100644 index 000000000..5bbb3f71b --- /dev/null +++ b/serverside_challenge_2/challenge/app/models/power_supply_plan.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class PowerSupplyPlan < ApplicationRecord + belongs_to :provider + has_many :meter_rate_charges + has_many :basic_charges + + def calculate_total_amount(amp, meter_rate) + basic_charge_record = basic_charges.where(amp: amp) + return nil if basic_charge_record.blank? + + basic_charge_amount = basic_charge_record.first.price.to_f + meter_rate_charge_amount = calculate_meter_rate_charge(meter_rate) + (basic_charge_amount + meter_rate_charge_amount).floor(0).to_i + end + + def calculate_meter_rate_charge(meter_rate) + total_meter_rate_charges = 0 + + meter_rate_charges.each do |step| + total_meter_rate_charges += step.calculate_charge(meter_rate.to_i) + end + total_meter_rate_charges + end +end diff --git a/serverside_challenge_2/challenge/app/models/provider.rb b/serverside_challenge_2/challenge/app/models/provider.rb new file mode 100644 index 000000000..445f24073 --- /dev/null +++ b/serverside_challenge_2/challenge/app/models/provider.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Provider < ApplicationRecord + has_many :power_supply_plans +end diff --git a/serverside_challenge_2/challenge/app/services/power_supply_plan_simulator.rb b/serverside_challenge_2/challenge/app/services/power_supply_plan_simulator.rb new file mode 100644 index 000000000..f990e9430 --- /dev/null +++ b/serverside_challenge_2/challenge/app/services/power_supply_plan_simulator.rb @@ -0,0 +1,60 @@ +class PowerSupplyPlanSimulator + VALID_AMPS = [10, 15, 20, 30, 40, 50, 60].freeze + + def initialize(amp, meter_rate) + @amp = amp + @meter_rate = meter_rate + @errors = [] + end + + def call + unless validate_params + return { + error: { + code: 400, + message: @errors.join('\n') + } + } + end + + result = [] + PowerSupplyPlan.includes(:provider, :meter_rate_charges, :basic_charges).each do |plan| + calculate_result = plan.calculate_total_amount(@amp, @meter_rate) + next unless calculate_result + + result << { + "provider_name": plan.provider.name, + "plan_name": plan.name, + "price": calculate_result + } + end + result + end + + def validate_params + if @amp.blank? + @errors << "リクエストパラメータ 'amp' が不足しています。" + else + @errors << "リクエストパラメータ 'amp' の値が不正です。'10 / 15 / 20 / 30 / 40 / 50 / 60' のいずれかの値を指定してください。" unless valid_amp?(@amp) + end + if @meter_rate.blank? + @errors << "リクエストパラメータ 'meter_rate' が不足しています。" + else + unless numeric?(@meter_rate) && @meter_rate.to_i >= 0 + @errors << "リクエストパラメータ 'meter_rate' の値が不正です。0以上の整数を指定してください。" + end + end + @errors.blank? + end + + def numeric?(string) + Integer(string) + true + rescue ArgumentError, TypeError + false + end + + def valid_amp?(amp) + VALID_AMPS.include?(amp.to_i) + end +end diff --git a/serverside_challenge_2/challenge/config/credentials.yml.enc b/serverside_challenge_2/challenge/config/credentials.yml.enc index 57c5aee24..3530d435b 100644 --- a/serverside_challenge_2/challenge/config/credentials.yml.enc +++ b/serverside_challenge_2/challenge/config/credentials.yml.enc @@ -1 +1 @@ -bZE5VzGsIQoeBEOa79I28cq/Ax0SuIaPhy5+NFktLVZLNGpuzqmkTkAiWc+arbGUDU8aaedg9zUvnBBc0dPrATeWISHtFxVwJMScZQeSW1k7BUgSD6B6YsewlgtpEDFDAXoGZEQoTbM2FY3lDCd0dZF7I1+DsB4I9Z5NamOgOlNEK8gMQD3p3Csqc/mVhmdI3PjBEmblvTjkZIVOdfX1CmqJ/RZm1Lxu3RdNMaiXiOZL7I+oXE1SFfjWm7+CTwb+Vfs6nGMviDHDKh9k3jyb7Vflpx/frY4vYzP8WMzgbJPX4UHmxa7qS51tJ1ECpK265nS1mLsqxL7wRbjyzfUNtXknSSopGD7sDDm7F9ta8/e1nzU79uTH1LKFpWQWGMSbXmSu4EKPu655BA9ckdwnSsf67RYNX7/Nw5pp--9AT1V0z/SBAdVTZ5--JK/4x776vcnN7qGZuylM0g== \ No newline at end of file +aLpSiYFmK7YeuO/q62KJR/UyXPpBWhH+oS6rzGWSQH/9p53vCpRMvfaUsZSneP9wYMioohjDqHdeY0bpOAXjMPy5suQk7yGANivapLmd2qNLt1wpIYzKGJiEwSP2gQD4Hyo/K6qGF2fchngDFhy0WWAe8Q0kcIM7WzUSV+VilcYylHcHHpdYhjsBx4+7ev2dbWLlYxbYuFSDGklGP85oYBmIH2QIzcYmwHhBkjwzFWvIl3xqL+/8iQlYsFyBsTk3tntgD/Au3kv6qYXQnf3ITXgHTKlaooxLTy9ZYwMOhJvPwtkMsWv9CVY+ACkUi5h9WY+rSHiBVCDHJwnEhwpz8/n4kzGXLuGRburdDZkA1IZb+j7+KzX625Y+x4+zR7P1wc8vJRw+/5O4MSndI28y0KkX15Qd8vMYNrowsXx+0WFtvo8LTh0J6p84b4bJyVkD1bGUpvpz+UADtGfTbuDqqRXNDCRM7NITVYcOg74QKiwi57gzRJgKOYt1KhXMLA5y4V+xo2zBZuTzGAqNS2rHcfwil9lcsmsTOQL7felWv2C2qqUllUo3CqTGNlXo3A24guP25tr3vDm2IrH7V/TLSjJcDAI1JUi35wwrCErgxrwK9eG0Y4w=--97QWVDZ45R2Bjr3G--byi/t6soITzi5vJv5PEsvw== \ No newline at end of file diff --git a/serverside_challenge_2/challenge/config/routes.rb b/serverside_challenge_2/challenge/config/routes.rb index 262ffd547..e73a37fdf 100644 --- a/serverside_challenge_2/challenge/config/routes.rb +++ b/serverside_challenge_2/challenge/config/routes.rb @@ -3,4 +3,9 @@ # Defines the root path route ("/") # root "articles#index" + namespace :api do + namespace :v1 do + get "simulate_all_plan", to: "power_supply_plan#simulate_all" + end + end end diff --git a/serverside_challenge_2/challenge/db/migrate/20250723134304_create_basic_charges.rb b/serverside_challenge_2/challenge/db/migrate/20250723134304_create_basic_charges.rb new file mode 100644 index 000000000..8b9371f43 --- /dev/null +++ b/serverside_challenge_2/challenge/db/migrate/20250723134304_create_basic_charges.rb @@ -0,0 +1,34 @@ +class CreateBasicCharges < ActiveRecord::Migration[7.0] + def change + create_table :providers do |t| + t.string :provider_name, :null => false + t.string :rounding_amount_method, :null => false + + t.timestamps + end + + create_table :power_supply_plans do |t| + t.belongs_to :provider + t.string :plan_name, :null => false + + t.timestamps + end + + create_table :meter_rate_charges do |t| + t.belongs_to :power_supply_plan + t.integer :min_meter_rate, :null => false + t.integer :max_meter_rate + t.decimal :price, precision: 6, scale: 2, :null => false + + t.timestamps + end + + create_table :basic_charges do |t| + t.belongs_to :power_supply_plan + t.integer :amp, :null => false + t.decimal :price, precision: 6, scale: 2, :null => false + + t.timestamps + end + end +end diff --git a/serverside_challenge_2/challenge/db/migrate/20250726103216_remove_rounding_amount_method_from_provider.rb b/serverside_challenge_2/challenge/db/migrate/20250726103216_remove_rounding_amount_method_from_provider.rb new file mode 100644 index 000000000..d9d065ba2 --- /dev/null +++ b/serverside_challenge_2/challenge/db/migrate/20250726103216_remove_rounding_amount_method_from_provider.rb @@ -0,0 +1,9 @@ +class RemoveRoundingAmountMethodFromProvider < ActiveRecord::Migration[7.0] + def up + remove_column :providers, :rounding_amount_method, :string + end + + def down + add_column :providers, :rounding_amount_method, :string + end +end \ No newline at end of file diff --git a/serverside_challenge_2/challenge/db/migrate/20250726111141_rename_provider_name_column_to_name.rb b/serverside_challenge_2/challenge/db/migrate/20250726111141_rename_provider_name_column_to_name.rb new file mode 100644 index 000000000..4f338026f --- /dev/null +++ b/serverside_challenge_2/challenge/db/migrate/20250726111141_rename_provider_name_column_to_name.rb @@ -0,0 +1,5 @@ +class RenameProviderNameColumnToName < ActiveRecord::Migration[7.0] + def change + rename_column :providers, :provider_name, :name + end +end diff --git a/serverside_challenge_2/challenge/db/migrate/20250726111346_rename_plan__name_column_to_name.rb b/serverside_challenge_2/challenge/db/migrate/20250726111346_rename_plan__name_column_to_name.rb new file mode 100644 index 000000000..2bd08e0d5 --- /dev/null +++ b/serverside_challenge_2/challenge/db/migrate/20250726111346_rename_plan__name_column_to_name.rb @@ -0,0 +1,5 @@ +class RenamePlanNameColumnToName < ActiveRecord::Migration[7.0] + def change + rename_column :power_supply_plans, :plan_name, :name + end +end diff --git a/serverside_challenge_2/challenge/db/schema.rb b/serverside_challenge_2/challenge/db/schema.rb new file mode 100644 index 000000000..ee18ba699 --- /dev/null +++ b/serverside_challenge_2/challenge/db/schema.rb @@ -0,0 +1,50 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.0].define(version: 2025_07_26_111346) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "basic_charges", force: :cascade do |t| + t.bigint "power_supply_plan_id" + t.integer "amp", null: false + t.decimal "price", precision: 6, scale: 2, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["power_supply_plan_id"], name: "index_basic_charges_on_power_supply_plan_id" + end + + create_table "meter_rate_charges", force: :cascade do |t| + t.bigint "power_supply_plan_id" + t.integer "min_meter_rate", null: false + t.integer "max_meter_rate" + t.decimal "price", precision: 6, scale: 2, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["power_supply_plan_id"], name: "index_meter_rate_charges_on_power_supply_plan_id" + end + + create_table "power_supply_plans", force: :cascade do |t| + t.bigint "provider_id" + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["provider_id"], name: "index_power_supply_plans_on_provider_id" + end + + create_table "providers", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/serverside_challenge_2/challenge/db/seeds.rb b/serverside_challenge_2/challenge/db/seeds.rb index bc25fce30..5b2bbf57e 100644 --- a/serverside_challenge_2/challenge/db/seeds.rb +++ b/serverside_challenge_2/challenge/db/seeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). # @@ -5,3 +7,99 @@ # # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) # Character.create(name: "Luke", movie: movies.first) + +p '==================== provider create ====================' + +Provider.create!( + [ + { name: '東京電力エナジーパートナー' }, + { name: '東京ガス' }, + { name: 'Looopでんき' } + ] +) + +p '==================== power_supply_plan create ====================' + +provider = Provider.find(1) +plans = provider.power_supply_plans.new( + [ + { name: '従量電灯B' }, + { name: 'スタンダードS' } + ] +) +plans.each(&:save) + +Provider.find(2).power_supply_plans.create!(name: 'ずっとも電気1') +Provider.find(3).power_supply_plans.create!(name: 'おうちプラン') + +p '==================== basic_charge create ====================' + +seed_data = [ + { index: 1, data: [ + { amp: 10, price: 286.00 }, + { amp: 15, price: 429.00 }, + { amp: 20, price: 572.00 }, + { amp: 30, price: 858.00 }, + { amp: 40, price: 1144.00 }, + { amp: 50, price: 1430.00 }, + { amp: 60, price: 1716.00 } + ] }, + { index: 2, data: [ + { amp: 10, price: 311.75 }, + { amp: 15, price: 467.63 }, + { amp: 20, price: 623.50 }, + { amp: 30, price: 935.25 }, + { amp: 40, price: 1247.00 }, + { amp: 50, price: 1558.75 }, + { amp: 60, price: 1870.50 } + ] }, + { index: 3, data: [ + { amp: 30, price: 858.00 }, + { amp: 40, price: 1144.00 }, + { amp: 50, price: 1430.00 }, + { amp: 60, price: 1716.00 } + ] }, + { index: 4, data: [ + { amp: 10, price: 0.00 }, + { amp: 15, price: 0.00 }, + { amp: 20, price: 0.00 }, + { amp: 30, price: 0.00 }, + { amp: 40, price: 0.00 }, + { amp: 50, price: 0.00 }, + { amp: 60, price: 0.00 } + ] } +] +seed_data.each do |d| + plan = PowerSupplyPlan.find(d[:index]) + basic_charges = plan.basic_charges.new(d[:data]) + basic_charges.each(&:save) +end + +p '==================== meter_ratecharge create ====================' + +seed_data = [ + { index: 1, data: [ + { min_meter_rate: 0, max_meter_rate: 120, price: 19.88 }, + { min_meter_rate: 121, max_meter_rate: 300, price: 26.48 }, + { min_meter_rate: 301, max_meter_rate: nil, price: 30.57 } + ] }, + { index: 2, data: [ + { min_meter_rate: 0, max_meter_rate: 120, price: 29.80 }, + { min_meter_rate: 121, max_meter_rate: 300, price: 36.40 }, + { min_meter_rate: 301, max_meter_rate: nil, price: 40.49 } + ] }, + { index: 3, data: [ + { min_meter_rate: 0, max_meter_rate: 140, price: 23.67 }, + { min_meter_rate: 141, max_meter_rate: 350, price: 23.88 }, + { min_meter_rate: 351, max_meter_rate: nil, price: 26.41 } + ] }, + { index: 4, data: [ + { min_meter_rate: 0, max_meter_rate: nil, price: 28.80 } + ] } +] + +seed_data.each do |d| + plan = PowerSupplyPlan.find(d[:index]) + basic_charges = plan.meter_rate_charges.new(d[:data]) + basic_charges.each(&:save) +end diff --git a/serverside_challenge_2/challenge/spec/models/meter_rate_charge.rb b/serverside_challenge_2/challenge/spec/models/meter_rate_charge.rb new file mode 100644 index 000000000..a5a346831 --- /dev/null +++ b/serverside_challenge_2/challenge/spec/models/meter_rate_charge.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe PowerSupplyPlan, type: :model do + let!(:provider1) { Provider.create!(id: 1, name: '東京ガス') } + let!(:provider2) { Provider.create!(id: 2, name: 'Looopでんき') } + let!(:plan1) { PowerSupplyPlan.create!(id: 3, provider_id: 1, name: 'ずっとも電気1') } + let!(:plan2) { PowerSupplyPlan.create!(id: 4, provider_id: 2, name: 'おうちプラン') } + let!(:meter_rate_charge1) { MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 0, max_meter_rate: 140, price: 23.67) } + let!(:meter_rate_charge2) { MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 141, max_meter_rate: 350, price: 23.88) } + let!(:meter_rate_charge3) { MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 351, max_meter_rate: nil, price: 26.41) } + let!(:meter_rate_charge4) { MeterRateCharge.create!(power_supply_plan_id: 4, min_meter_rate: 0, max_meter_rate: nil, price: 28.8) } + + describe '#calculate_charge' do + context 'calculate on 3 steps plan' do + it 'calculate result with 1st step' do + expect(meter_rate_charge1.calculate_charge(120).to_f).to eq 2840.4 + expect(meter_rate_charge1.calculate_charge(140).to_f).to eq 3313.8 + expect(meter_rate_charge1.calculate_charge(150).to_f).to eq 3313.8 + end + + it 'calculate result with middle step' do + expect(meter_rate_charge2.calculate_charge(150).to_f).to eq 238.8 + expect(meter_rate_charge2.calculate_charge(350).to_f).to eq 5014.8 + expect(meter_rate_charge2.calculate_charge(400).to_f).to eq 5014.8 + end + + it 'calculate result with highest step' do + expect(meter_rate_charge3.calculate_charge(351).to_f).to eq 26.41 + expect(meter_rate_charge3.calculate_charge(400).to_f).to eq 1320.5 + end + end + + context 'calculate on single step plan' do + it 'calculate result with single step plan' do + expect(meter_rate_charge4.calculate_charge(100).to_f).to eq 2880 + end + end + end +end diff --git a/serverside_challenge_2/challenge/spec/models/power_supply_plan.rb b/serverside_challenge_2/challenge/spec/models/power_supply_plan.rb new file mode 100644 index 000000000..331e86c5c --- /dev/null +++ b/serverside_challenge_2/challenge/spec/models/power_supply_plan.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe PowerSupplyPlan, type: :model do + let!(:provider) { Provider.create!(id: 1, name: '東京ガス') } + let!(:plan) { PowerSupplyPlan.create!(id: 3, provider_id: 1, name: 'ずっとも電気1') } + let!(:basic_charge1) { BasicCharge.create!(power_supply_plan_id: 3, amp: 30, price: 858.0) } + let!(:basic_charge2) { BasicCharge.create!(power_supply_plan_id: 3, amp: 40, price: 1144.0) } + let!(:basic_charge3) { BasicCharge.create!(power_supply_plan_id: 3, amp: 50, price: 1430.0) } + let!(:basic_charge4) { BasicCharge.create!(power_supply_plan_id: 3, amp: 60, price: 1716.0) } + let!(:meter_rate_charge1) do + MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 0, max_meter_rate: 140, price: 23.67) + end + let!(:meter_rate_charge2) do + MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 141, max_meter_rate: 350, price: 23.88) + end + let!(:meter_rate_charge3) do + MeterRateCharge.create!(power_supply_plan_id: 3, min_meter_rate: 351, max_meter_rate: nil, price: 26.41) + end + let(:amp) { 10 } + let(:meter_rate) { 120 } + describe '#calculate_total_amount' do + context 'when no meter rate charge available ' do + it 'returns nil' do + expect(plan.calculate_total_amount(amp, meter_rate)).to be nil + end + end + + context 'when meter rate charge available' do + it 'calculate with designated basic charge' do + allow(plan).to receive(:calculate_meter_rate_charge).with(meter_rate).and_return(1000.00) + expect(plan.calculate_total_amount(30, meter_rate).to_f).to eq 1858.00 + end + end + end + + describe '#calculate_meter_rate_charge' do + it 'returns calculation result with 1st step only' do + expect(plan.calculate_meter_rate_charge(meter_rate).to_f).to eq 2840.40 + end + it 'returns calculation result with 1st and 2nd step' do + expect(plan.calculate_meter_rate_charge(150).to_f).to eq 3552.6 + end + end +end diff --git a/serverside_challenge_2/challenge/spec/rails_helper.rb b/serverside_challenge_2/challenge/spec/rails_helper.rb new file mode 100644 index 000000000..0d761946b --- /dev/null +++ b/serverside_challenge_2/challenge/spec/rails_helper.rb @@ -0,0 +1,69 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file +# that will avoid rails generators crashing because migrations haven't been run yet +# return unless Rails.env.test? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = Rails.root.join('spec/fixtures') + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails uses metadata to mix in different behaviours to your tests, + # for example enabling you to call `get` and `post` in request specs. e.g.: + # + # RSpec.describe UsersController, type: :request do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/7-1/rspec-rails + # + # You can also this infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") + # +end diff --git a/serverside_challenge_2/challenge/spec/services/power_supply_plan_simulator.rb b/serverside_challenge_2/challenge/spec/services/power_supply_plan_simulator.rb new file mode 100644 index 000000000..fee48dd72 --- /dev/null +++ b/serverside_challenge_2/challenge/spec/services/power_supply_plan_simulator.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'PowerSupplyPlanSimulator' do + context 'when input parameter is valid' do + let(:service) { PowerSupplyPlanSimulator.new(amp, meter_rate) } + let(:result) { service.call } + let(:amp) { 10 } + let(:meter_rate) { 120 } + let(:provider1) { double(Provider, name: '東京電力エナジーパートナー') } + let(:provider2) { double(Provider, name: '東京ガス') } + let(:plan1) { double(PowerSupplyPlan, name: '従量電灯B', provider: provider1) } + let(:plan2) { double(PowerSupplyPlan, name: 'ずっとも電気1', provider: provider2) } + + it 'returns all valid array' do + allow(PowerSupplyPlan).to receive(:includes).and_return([plan1, plan2]) + allow(plan1).to receive(:calculate_total_amount).with(amp, meter_rate).and_return 4101.6 + allow(plan2).to receive(:calculate_total_amount).with(amp, meter_rate).and_return 4556.4 + + expect(result.length).to eq 2 + expect(result).to include({ "provider_name": '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 4101.6 }) + expect(result).to include({ "provider_name": '東京ガス', plan_name: 'ずっとも電気1', price: 4556.4 }) + end + + it 'returns result with basic_charge availble plans' do + allow(PowerSupplyPlan).to receive(:includes).and_return([plan1, plan2]) + allow(plan1).to receive(:calculate_total_amount).with(amp, meter_rate).and_return 4101.6 + allow(plan2).to receive(:calculate_total_amount).with(amp, meter_rate).and_return nil + + expect(result.length).to eq 1 + expect(result).to include({ "provider_name": '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 4101.6 }) + expect(result).not_to include({ "provider_name": '東京ガス', plan_name: 'ずっとも電気1', price: 4556.4 }) + end + end + + context 'when input parameter is invalid' do + let(:service) { PowerSupplyPlanSimulator.new(amp, meter_rate) } + let(:result) { service.call } + + context 'invalid amp' do + let(:amp) { -10 } + let(:meter_rate) { 120 } + + it 'validate_params to be false and return error hash ' do + expect(result.is_a?(Hash)).to be_truthy + end + + it 'returns amp invalid error' do + expect(result.is_a?(Hash)).to be_truthy + expect(result[:error][:message]).to start_with "リクエストパラメータ 'amp' の値が不正です。" + end + end + + context 'invalid meter_rate' do + let(:amp) { 10 } + let(:meter_rate) { -120 } + + it 'validate_params to be false' do + expect(result.is_a?(Hash)).to be_truthy + end + + it 'return meter_rate invalid error' do + expect(result[:error][:message]).to start_with "リクエストパラメータ 'meter_rate' の値が不正です。" + end + end + + context 'amp is missing' do + let(:amp) { nil } + let(:meter_rate) { 120 } + + it 'validate_params to be false' do + expect(result.is_a?(Hash)).to be_truthy + end + + it 'return amp missing error' do + expect(result.is_a?(Hash)).to be_truthy + expect(result[:error][:message]).to eq "リクエストパラメータ 'amp' が不足しています。" + end + end + + context 'meter_rate is missing' do + let(:amp) { 10 } + let(:meter_rate) { nil } + + it 'validate_params to be false' do + expect(result.is_a?(Hash)).to be_truthy + end + + it 'return amp missing error' do + expect(result.is_a?(Hash)).to be_truthy + expect(result[:error][:message]).to eq "リクエストパラメータ 'meter_rate' が不足しています。" + end + end + end +end diff --git a/serverside_challenge_2/challenge/spec/spec_helper.rb b/serverside_challenge_2/challenge/spec/spec_helper.rb new file mode 100644 index 000000000..327b58ea1 --- /dev/null +++ b/serverside_challenge_2/challenge/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end