diff --git a/serverside_challenge_2/challenge/.gitignore b/serverside_challenge_2/challenge/.gitignore index 88381219c..9662fde16 100644 --- a/serverside_challenge_2/challenge/.gitignore +++ b/serverside_challenge_2/challenge/.gitignore @@ -27,3 +27,9 @@ # Ignore master key for decrypting credentials and more. /config/master.key + +# Ignore Editor's filepath +.vscode +.idea + +.tool-versions \ No newline at end of file diff --git a/serverside_challenge_2/challenge/README.md b/serverside_challenge_2/challenge/README.md index 7db80e4ca..09e06eb3d 100644 --- a/serverside_challenge_2/challenge/README.md +++ b/serverside_challenge_2/challenge/README.md @@ -1,24 +1,73 @@ -# README +# 電気料金出力API +## 概要 +与えられたリクエスト値を元に、一致する電力会社/プランの料金を返却するAPIです。 -This README would normally document whatever steps are necessary to get the -application up and running. +## API仕様 +### エンドポイント +``` +GET electricity_charges/calculate +``` -Things you may want to cover: +### リクエスト値 -* Ruby version +| パラメータ名 | 説明 | データ型 | パラメータ制限 | 備考 | +|--------|---------|------|----------------------------|---------| +| ampere | 契約アンペア数 | int | 10/15/20/30/40/50/60 のいずれか | 単位: A | +| usage | 電力使用量 | int | 0以上の整数 | 単位: kWh | -* System dependencies +### レスポンス +| パラメータ名 | 説明 | データ型 | 備考 | +|---------------|-------|--------|----| +| provider_name | 電力会社名 | string | | +| plan_name | プラン名 | string | | +| price | 値段 | float | | -* Configuration +### レスポンスのサンプル +#### 一致するプランがある場合 -* Database creation +```json lines +// GET http://localhost:3000/electricity_charges/calculate?ampere=30&usage=10 -* Database initialization +[ + { + "provider_name": "東京電力エナジーパートナー", + "plan_name": "従量電灯B", + "price": 1056.8 + }, + { + "provider_name": "東京電力エナジーパートナー", + "plan_name": "スタンダードS", + "price": 1233.25 + }, + { + "provider_name": "東京ガス", + "plan_name": "ずっとも電気1", + "price": 1094.7 + }, + { + "provider_name": "Looopでんき", + "plan_name": "おうちプラン", + "price": 288.0 + } +] +``` -* How to run the test suite +#### 一致するプランがない場合 -* Services (job queues, cache servers, search engines, etc.) +```json +[ + { + "message": "ampereに一致するプランが登録されていません" + } +] +``` -* Deployment instructions -* ... +#### パラメータが不正な場合 + +```json + +{ + "error": "ampereの値が正しくありません" +} +``` \ No newline at end of file diff --git a/serverside_challenge_2/challenge/app/controllers/electricity_charges/calculate_controller.rb b/serverside_challenge_2/challenge/app/controllers/electricity_charges/calculate_controller.rb new file mode 100644 index 000000000..8b9294eef --- /dev/null +++ b/serverside_challenge_2/challenge/app/controllers/electricity_charges/calculate_controller.rb @@ -0,0 +1,13 @@ +class ElectricityCharges::CalculateController < ApplicationController + include ElectricChargesHelper + def calc + error, status = validate_params(params) + if error + return render json: error, status: status + end + + calc_result, calc_status = import_price_from_yaml(params[:ampere].to_i, params[:usage].to_i) + + render json: calc_result, status: calc_status + end +end diff --git a/serverside_challenge_2/challenge/app/helpers/electric_charges_helper.rb b/serverside_challenge_2/challenge/app/helpers/electric_charges_helper.rb new file mode 100644 index 000000000..66e2abe23 --- /dev/null +++ b/serverside_challenge_2/challenge/app/helpers/electric_charges_helper.rb @@ -0,0 +1,74 @@ +module ElectricChargesHelper + VALID_CONTRACT_AMPERES = %w(10 15 20 30 40 50 60).freeze + VALID_USAGE_REGEX = /\A\d+\z/.freeze + + # リクエストパラメータのバリデーション + def validate_params(request_params) + # パラメータが不足しているとき + if request_params[:ampere].nil? || request_params[:usage].nil? + return { error: I18n.t('errors.empty_parameter') }, :bad_request + end + + # ampereは特定値である必要がある + unless VALID_CONTRACT_AMPERES.include?(request_params[:ampere]) + return { error: I18n.t('errors.invalid_contract_ampere') }, :bad_request + end + + # usageは0以上の整数である必要がある + usage = request_params[:usage].to_i + unless VALID_USAGE_REGEX.match?(request_params[:usage]) + return { error: I18n.t('errors.invalid_usage') }, :bad_request + end + if usage < 0 + return { error: I18n.t('errors.invalid_usage') }, :bad_request + end + + nil + end + + # yamlからパラメータ読み込み + def import_price_from_yaml(ampere, usage) + results = [] + electricity_charges_data = YAML.load_file("config/electricity_charges.yml") + # 基本料金 + electricity_charges_data["basic_price"].each do |provider| + provider["plans"].each do |plan| + if plan["amperes"].has_key?(ampere) + results << { + "provider_name": provider["provider_name"], + "plan_name": plan["plan"], + "price": plan["amperes"][ampere] + } + end + end + end + + # ampereのパラメータに紐づくデータがない場合はその旨を返す + if results.empty? + results << { message: I18n.t('message.empty_result_by_ampere') } + return results, 404 + end + + # 従量料金 + results.each do |result| + provider_usage_price_data = electricity_charges_data["usage_price"].select{|provider| provider["provider_name"] == result[:provider_name]} + provider_usage_price_data.each do |data| + plans = data["plans"].select {|plan| plan["plan"] == result[:plan_name]} + plans.each do |plan| + thresholds = plan["thresholds"] + keys = thresholds.keys.map(&:to_i).sort.reverse + keys.each do |key| + if usage >= key + unit_price_kwh = thresholds[key] + usage_price = (unit_price_kwh * usage).round(2) + result[:price] += usage_price + break + end + end + end + end + end + + [results, 200] + end +end diff --git a/serverside_challenge_2/challenge/config/electricity_charges.yml b/serverside_challenge_2/challenge/config/electricity_charges.yml new file mode 100644 index 000000000..40f856395 --- /dev/null +++ b/serverside_challenge_2/challenge/config/electricity_charges.yml @@ -0,0 +1,65 @@ +basic_price: + - provider_name: 東京電力エナジーパートナー + plans: + - plan: 従量電灯B + amperes: + 10: 286.00 + 15: 429.00 + 20: 572.00 + 30: 858.00 + 40: 1144.00 + 50: 1430.00 + 60: 1716.00 + - plan: スタンダードS + amperes: + 10: 311.75 + 15: 467.63 + 20: 623.50 + 30: 935.25 + 40: 1247.00 + 50: 1558.75 + 60: 1870.50 + - provider_name: 東京ガス + plans: + - plan: ずっとも電気1 + amperes: + 30: 858.00 + 40: 1144.00 + 50: 1430.00 + 60: 1716.00 + - provider_name: Looopでんき + plans: + - plan: おうちプラン + amperes: + 10: 0.00 + 15: 0.00 + 20: 0.00 + 30: 0.00 + 40: 0.00 + 50: 0.00 + 60: 0.00 +usage_price: + - provider_name: 東京電力エナジーパートナー + plans: + - plan: 従量電灯B + thresholds: + 0: 19.88 + 121: 26.48 + 301: 30.57 + - plan: スタンダードS + thresholds: + 0: 29.80 + 121: 36.40 + 301: 40.49 + - provider_name: 東京ガス + plans: + - plan: ずっとも電気1 + thresholds: + 0: 23.67 + 141: 23.88 + 351: 26.41 + - provider_name: Looopでんき + plans: + - plan: おうちプラン + thresholds: + 0: 28.8 \ No newline at end of file diff --git a/serverside_challenge_2/challenge/config/locales/en.yml b/serverside_challenge_2/challenge/config/locales/ja.yml similarity index 64% rename from serverside_challenge_2/challenge/config/locales/en.yml rename to serverside_challenge_2/challenge/config/locales/ja.yml index 8ca56fc74..abb35d481 100644 --- a/serverside_challenge_2/challenge/config/locales/en.yml +++ b/serverside_challenge_2/challenge/config/locales/ja.yml @@ -30,4 +30,11 @@ # available at https://guides.rubyonrails.org/i18n.html. en: - hello: "Hello world" + errors: + empty_parameter: 'リクエストパラメータにampereとusageを設定してください' + invalid_parameter_type: 'ampereとusageはいずれも数値を設定してください' + invalid_contract_ampere: 'ampereの値が正しくありません' + invalid_usage: 'usageは0以上の整数を設定してください' + message: + empty_result_by_ampere: 'ampereに一致するプランが登録されていません' + diff --git a/serverside_challenge_2/challenge/config/routes.rb b/serverside_challenge_2/challenge/config/routes.rb index 262ffd547..f5caa9839 100644 --- a/serverside_challenge_2/challenge/config/routes.rb +++ b/serverside_challenge_2/challenge/config/routes.rb @@ -1,6 +1,5 @@ Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Defines the root path route ("/") - # root "articles#index" + namespace :electricity_charges do + get 'calculate', to: 'calculate#calc' + end end diff --git a/serverside_challenge_2/challenge/test/controllers/electricity_charges/calculate_controller_test.rb b/serverside_challenge_2/challenge/test/controllers/electricity_charges/calculate_controller_test.rb new file mode 100644 index 000000000..72e654be0 --- /dev/null +++ b/serverside_challenge_2/challenge/test/controllers/electricity_charges/calculate_controller_test.rb @@ -0,0 +1,80 @@ +require "test_helper" + +class ElectricityCharges::CalculateControllerTest < ActionDispatch::IntegrationTest + test '正常系: 契約アンペア数 10A, 使用量 0kWh' do + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 286.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 311.75 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 0.0 }, + ] + get electricity_charges_calculate_url, params: { ampere: 10, usage: 0 } + assert_response :success + assert_equal(expected.to_json, response.body) + end + + test '正常系: 契約アンペア数 30A, 使用量 100kWh' do + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 2846.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 3915.25 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 3225.0 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 2880.0 }, + ] + get electricity_charges_calculate_url, params: { ampere: 30, usage: 100 } + assert_response :success + assert_equal(expected.to_json, response.body) + end + + test '正常系: 契約アンペア数 60A, 使用量 1000kWh' do + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 32286.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 42360.5 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 28126.0 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 28800.0 }, + ] + get electricity_charges_calculate_url, params: { ampere: 60, usage: 1000 } + assert_response :success + assert_equal(expected.to_json, response.body) + end + + test '異常系: 契約アンペア数 0A' do + get electricity_charges_calculate_url, params: { ampere: 0, usage: 100 } + assert_response :bad_request + assert_equal({ 'error' => 'ampereの値が正しくありません' }.to_json, response.body) + end + + test '異常系: 契約アンペア数 70A' do + get electricity_charges_calculate_url, params: { ampere: 70, usage: 100 } + assert_response :bad_request + assert_equal({ 'error' => 'ampereの値が正しくありません' }.to_json, response.body) + end + + test '異常系: 使用量 -1kWh' do + get electricity_charges_calculate_url, params: { ampere: 30, usage: -1 } + assert_response :bad_request + assert_equal({ 'error' => 'usageは0以上の整数を設定してください' }.to_json, response.body) + end + + test '異常系: 使用量 1.5kWh (型が不正)' do + get electricity_charges_calculate_url, params: { ampere: 30, usage: 1.5 } + assert_response :bad_request + assert_equal({ 'error' => 'usageは0以上の整数を設定してください' }.to_json, response.body) + end + + test '異常系: 契約アンペア数 "abc" (型が不正)' do + get electricity_charges_calculate_url, params: { ampere: "abc", usage: 100 } + assert_response :bad_request + assert_equal({ 'error' => 'ampereの値が正しくありません' }.to_json, response.body) + end + + test '異常系: 使用量 "abc" (型が不正)' do + get electricity_charges_calculate_url, params: { ampere: 30, usage: "abc" } + assert_response :bad_request + assert_equal({"error"=>"usageは0以上の整数を設定してください"}.to_json, response.body) + end + + test '異常系: パラメータが空' do + get electricity_charges_calculate_url, params: {} + assert_response :bad_request + assert_equal({"error"=>"リクエストパラメータにampereとusageを設定してください"}.to_json, response.body) + end +end diff --git a/serverside_challenge_2/challenge/test/helper/electric_charges_helper_test.rb b/serverside_challenge_2/challenge/test/helper/electric_charges_helper_test.rb new file mode 100644 index 000000000..d8fc7a6e3 --- /dev/null +++ b/serverside_challenge_2/challenge/test/helper/electric_charges_helper_test.rb @@ -0,0 +1,123 @@ +require "test_helper" + +class ElectricityChargesHelperTest < ActionDispatch::IntegrationTest + include ElectricChargesHelper + test '[validate_params]正常系: 契約アンペア数 30A, 使用量 100kWh' do + params = { ampere: "30", usage: "100" } + assert_nil validate_params(params) + end + + test '[validate_params]異常系: 契約アンペア数 0A' do + params = { ampere: "0", usage: "100" } + error, status = validate_params(params) + assert_equal({ error: 'ampereの値が正しくありません' }, error) + assert_equal :bad_request, status + end + + test '[validate_params]異常系: 使用量 -1kWh' do + params = { ampere: "30", usage: "-1" } + error, status = validate_params(params) + assert_equal({ error: 'usageは0以上の整数を設定してください' }, error) + assert_equal :bad_request, status + end + + test '[validate_params]異常系: 契約アンペア数 "abc" (型が不正)' do + params = { ampere: "abc", usage: "100" } + error, status = validate_params(params) + assert_equal({ error: 'ampereの値が正しくありません' }, error) + assert_equal :bad_request, status + end + + test '[validate_params]異常系: 使用量 "abc" (型が不正)' do + params = { ampere: "30", usage: "abc" } + error, status = validate_params(params) + assert_equal({ error: 'usageは0以上の整数を設定してください' }, error) + assert_equal :bad_request, status + end + + test '[validate_params]異常系: パラメータが空' do + params = {} + error, status = validate_params(params) + assert_equal({ error: 'リクエストパラメータにampereとusageを設定してください' }, error) + assert_equal :bad_request, status + end + + test '[import_price_from_yaml] 正常系テスト1: 契約アンペア数30A, 使用量100kWh' do + result, status = import_price_from_yaml(30, 100) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 2846.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 3915.25 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 3225.0 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 2880.0 }, + ] + assert_equal expected, result + assert_equal 200, status + end + + test '[import_price_from_yaml] 正常系テスト2: 契約アンペア数10A, 使用量0kWh' do + result, status = import_price_from_yaml(10, 0) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 286.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 311.75 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 0.0 }, + ] + assert_equal expected, result + assert_equal 200, status + end + + test '[import_price_from_yaml] 正常系テスト3: 契約アンペア数60A, 使用量1000kWh' do + result, status = import_price_from_yaml(60, 1000) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 32286.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 42360.5 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 28126.0 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 28800.0 }, + ] + assert_equal expected, result + assert_equal 200, status + end + + test '[import_price_from_yaml] 正常系テスト4: 契約アンペア数40A, 使用量200kWh' do + result, status = import_price_from_yaml(40, 200) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 6440.0 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 8527.0 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 5920.0 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 5760.0 }, + ] + assert_equal expected, result + assert_equal 200, status + end + + test '[import_price_from_yaml] 異常系テスト: アンペア数に一致するプランがない場合' do + result, status = import_price_from_yaml(5, 10) + expected = [ + { message: "ampereに一致するプランが登録されていません"}, + ] + assert_equal expected, result + assert_equal 404, status + end + + test '[import_price_from_yaml] 境界値テスト1: 契約アンペア数10A, 使用量120kWh' do + result, status = import_price_from_yaml(10, 120) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 2671.6 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 3887.75 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 3456.0 }, + ] + assert_equal expected, result + assert_equal 200, status + end + + test '[import_price_from_yaml] 境界値テスト2: 契約アンペア数60A, 使用量351kWh' do + result, status = import_price_from_yaml(60, 351) + expected = [ + { provider_name: '東京電力エナジーパートナー', plan_name: '従量電灯B', price: 12446.07 }, + { provider_name: '東京電力エナジーパートナー', plan_name: 'スタンダードS', price: 16082.49 }, + { provider_name: '東京ガス', plan_name: 'ずっとも電気1', price: 10985.91 }, + { provider_name: 'Looopでんき', plan_name: 'おうちプラン', price: 10108.8 }, + ] + assert_equal expected, result + assert_equal 200, status + end +end \ No newline at end of file