Skip to content

feat: サーバーサイド課題 2 の実装#25

Merged
en-Barry merged 105 commits into
masterfrom
develop
May 13, 2026
Merged

feat: サーバーサイド課題 2 の実装#25
en-Barry merged 105 commits into
masterfrom
develop

Conversation

@en-Barry
Copy link
Copy Markdown
Owner

@en-Barry en-Barry commented May 12, 2026

Summary

エネチェンジ サーバーサイドコーディング課題 2「電気料金シミュレーション API」を実装し、develop の内容を master に統合する。

含まれる PR

PR 概要
#2 RSpec / PostgreSQL / RuboCop / Docker など開発基盤の整備
#8 料金マスタ (Provider / Plan / AmpereBasedRate / UsageBasedRate) を ActiveYaml で実装
#11 ElectricityBillSimulator サービスを実装 (要件: 切り捨て / アンペア基本料金 + 段階従量)
#12 GET /api/v1/electricity_bill_simulations エンドポイントと Form Object によるリクエスト検証
#14 ActiveYaml / YAML から ActiveRecord / PostgreSQL へのマスタ移行 (seed + FactoryBot)
#20 Rails 本番化対応 (rack-cors / /healthz / Dockerfile.production)
#21 フロントエンド (Next.js 16 + shadcn/ui + React Hook Form + Zod) を実装
#22 Terraform で AWS インフラ (ECS / RDS / ALB / CloudFront / Secrets Manager) を構築
#23 本番デプロイと疎通確認 (Vercel + AWS)

主要な設計判断

  • 電気料金は1円未満切り捨て (.floor) — 課題要件
  • 料金マスタは ActiveRecord (PostgreSQL) で永続化db/seeds.rb で YAML 由来のデータを冪等に投入。子→親の destroy_all で削除同期
  • API リクエスト検証は Form Object (app/forms/) に分離 — コントローラを薄く保つ
  • 従量課金のみプランampere_based_rates レコードの有無で判定 (Plan#metered_only?) — フラグを別途持たせない
  • HTTPS 強制は CloudFront に委譲 — Rails 側 force_ssl=falseassume_ssl=trueX-Forwarded-Proto を信頼

Test plan

  • bundle exec rspec がグリーン
  • bundle exec rubocop がグリーン
  • docker compose up でローカル環境が起動し /api/v1/electricity_bill_simulations?ampere=30&kwh=300 で 200 が返る
  • フロント npm run dev から API を叩いて結果表示
  • 本番 URL での疎通確認 (README に手順あり)

🤖 Generated with Claude Code

en-Barry and others added 30 commits May 2, 2026 01:47
image: postgres (latest) で PG18+ が引かれるが、PG18+ ではデータ配置が変わり既存ボリュームと互換性が無い。安定版 16 に固定して Docker 環境を再現可能にする。
rails g rspec:install で雛形を生成、test/ ディレクトリ一式を削除し、generators デフォルトを RSpec に切替。テストフレームワークを RSpec に一本化するため。
Claude Code のローカル設定 (.claude/settings.local.json) はリポジトリに含めない。
TablePlus 等の DB クライアントから localhost:5432 で接続できるようにする。
depends_on の単純な記法は PostgreSQL の起動完了 (accepting connections) を待たない。クリーン環境で docker compose run --rm web bin/rails db:create を実行した際に接続失敗するのを防ぐため、pg_isready ベースの healthcheck を追加し、web 側で service_healthy 条件を待つようにする。
- pg gem: `~> 1.1`(1.5.4)→ `>= 1.6.0`(1.6.3)
  - PG18 の wire protocol 3.2 変更(cancel request key 256bit 化)に対応
- docker-compose.yml: postgres:16 → postgres:18.3
- README: 環境セクションの PostgreSQL バージョンを 16 → 18 に修正

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PG18 以降はデータをメジャーバージョン別サブディレクトリに格納するため、
マウント先を /var/lib/postgresql/data → /var/lib/postgresql に変更。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pg 2.0 系リリース時の破壊的変更から保護するため上限を設ける。
PostgreSQL 18 対応で下限を 1.6.0 に引き上げた理由とは独立した制約。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
documentation format はテスト数が増えると CI ログが数千行になる。
ローカルで詳細出力が必要な場合は .rspec-local で上書き可能。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FactoryBot を使用するため fixture は不要。
generators で fixture: false を設定済みであり設定を一致させる。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- order: :random でテストの順序依存を検知する
- filter_run_when_matching :focus で fit/fdescribe 消し忘れを防ぐ
- Kernel.srand で seed 指定による失敗再現を可能にする

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9b22116 で Gemfile に `< 2.0` 上限を追加したが Gemfile.lock のコミットが漏れていた。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
active_hash gem を導入し、Provider / Plan / AmpereBasedRate / UsageBasedRate を
ActiveYaml モデルとして実装、料金体系を db/data/ 配下の YAML に格納する。

設計判断:
- rate は浮動小数点誤差を避けるため YAML 上は文字列で記述し、getter で BigDecimal に変換する
- 「従量課金のみプラン」(Looopでんき) は ampere_based_rates レコードの有無を真実とし、
  Plan#metered_only? でその意味づけを与える(フラグを別途持たせると整合性が壊れ得るため)
- ActiveHash の belongs_to / has_many は ActiveRecord の挙動と異なるため、
  Rails/RedundantPresenceValidationOnBelongsTo / Rails/HasManyOrHasOneDependent を該当ファイルで除外
- 後段で日本語のエラーメッセージを返すため、デフォルトロケールを ja に統一

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
データ整合性 spec では initializer での自動 fail を避け、テスト時に全レコードの
valid? を検証することで本番起動を不能にしないようにする(参照整合性とプラン数、
metered_only? の業務的不変条件もここで担保)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
YAML を初回読み込みするまでアクセサが定義されない暗黙の挙動に依存しないようにする。
Klass.all を先に呼ばずに Klass.new(...) ができ、ファイルを開くだけでスキーマが
分かるようになる。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
config.i18n.default_locale = :ja に切り替えたものの、Rails / ActiveModel が
同梱する標準メッセージ翻訳には ja が含まれず "Translation missing" になるため、
rails-i18n を追加して presence / inclusion 等のメッセージを日本語化する。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
expect(record).to be_valid, MSG の MSG は呼び出し前に eager 評価されるため、
matcher 側が valid? を呼ぶ前の状態(errors が空)で組み立てられていた。
先に valid? を呼んで errors を確定させてから文字列補間する。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
YAML データの型や境界値の不正をモデル単体で検出できるようにする。

設計判断:
- rate は getter が BigDecimal 変換を行うため、numericality が getter を呼ぶと
  "abc" のような変換不能値で ArgumentError になる。代わりに raw 値を直接読む
  custom validator (rate_must_be_number) を実装し、:not_a_number /
  :greater_than_or_equal_to で表現する
- presence 等の他バリデーションも同様に getter で落ちるため、
  read_attribute_for_validation を rate キーだけ raw 値に差し替える
- foreign key と数値属性の numericality は presence と併用するため
  allow_nil: true を付けてエラーを重複させない
- 料金レンジの連続性 (low=1 起点 + 段の間に穴/重複なし) を data_consistency_spec
  に加え、kWh 範囲のデータ的不変条件を保証する

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
これまでは spec/models/ のスポットチェック (first / find(1)) と
data_consistency の全レコード valid? からの間接保証だけだったため、
誤って rate getter を変更したときの検出網が弱かった。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
サービス未実装のため、この時点では NameError で全 fail する想定。
TDD の red を履歴として残すために実装より先にコミットする。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes #6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Integer() は Float を黙って切り捨てるため、parse_integer! で Float を事前チェックする。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
横断 / 全プラン対応のアンペアでは 4 プラン全て返る (ampere: 30) で
同等のカバレッジが得られるため重複テストを除去。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UsageBasedRate#low_must_not_exceed_high を `>` から `>=` に変更し
  low と high が同値のセグメント(範囲 0kWh)を invalid とする
- ja.yml のエラーメッセージを「以下でなければなりません」→
  「より小さくなければなりません」に修正(仕様と一致させる)
- モデル spec の #rate テストを find(id) / first 依存から
  new(...) による任意値渡しに変更し YAML データ変更で壊れない構造にする
- plan_spec のアソシエーション検証を件数・具体名から型(be_a)のみに絞り、
  件数の正確性は data_consistency_spec に委譲する
- plan_spec の #metered_only? を特定 ID 列挙から
  new(id: 9999) / all.find による振る舞いベースの検証に変更する

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
en-Barry and others added 26 commits May 12, 2026 19:40
substr で切り詰めると末尾ハイフンになりエラーになる問題を修正。
name_short ローカル(name_prefix を 25 文字以内に切り詰め末尾ハイフンを除去)を
導入し、ALB・TG 名がそれぞれ最大 29 / 32 文字に収まるようにした。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rails 7.0 は assume_ssl 未対応のため force_ssl=true だと ALB ドメインへ
誤リダイレクトが発生する。CloudFront の viewer_protocol_policy=redirect-to-https
が HTTPS 強制を担うため Rails 側の force_ssl は不要。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
クリーンな terraform apply 直後に ECS タスクが AWSCURRENT の欠如で
起動失敗する問題を修正。aws_secretsmanager_secret_version をプレースホルダー
値で追加し lifecycle.ignore_changes で手動投入値を保護する。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
X-Forwarded-Proto の伝播経路(CloudFront→ALB→ECS)と Rack が
無条件にヘッダーを読む挙動を説明し、secure cookie/HSTS への
影響がない理由を明示する。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CloudFront→ALB 間は HTTP-only のため ALB が付与する
X-Forwarded-Proto は "http" になり request.ssl? == false となる。
前回コメントの「CloudFront が X-Forwarded-Proto: https を付与」は
技術的に誤りだったため修正する。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…を制御可能にする

PLACEHOLDER + ignore_changes では ECS タスクが DB 接続失敗で
起動できず IaC の再現性が不十分だった。
sensitive 変数で値を注入する方式に変更し、変数未設定時は
count=0 で secret version を作成しない。
ecs_desired_count 変数(default=0)を追加しシークレット設定前は
ECS サービスを起動しない設計にする。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
username=postgres / db_name=app(modules/db/main.tf の実値)に修正。
appuser / appdb は存在しないため接続失敗する誤った例だった。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
開発環境(docker-compose: postgres:18.3)と本番環境(RDS)を
PostgreSQL 18 で統一する。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- :latest push 後は terraform apply では ECS が新イメージを pull しないため
  --force-new-deployment に変更
- ワンオフタスク手順の terraform output パスを challenge/ 前提に合わせ
  cd serverside_challenge_2/terraform → cd ../terraform に修正

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
APIエンドポイントとリクエストバリデーションを実装
ActiveHash/YAML から ActiveRecord/PostgreSQL へのDB移行
…uction-ready

Rails 本番化対応 (rack-cors / /healthz / Dockerfile.production)
…extjs

フロントエンド構築 (Next.js + shadcn/ui)
…aform

Terraform で AWS ECS / RDS / ALB / CloudFront 構築 (#18)
…duction

本番デプロイと疎通確認 (Vercel + AWS) (closes #19)
- HashAlignment / SymbolArray (%i 表記) / EmptyLines / LeadingSubject 等
- 振る舞いは変えていない (RSpec 74 examples 引き続きグリーン)
マイグレーションは Rails が生成する DSL を素直に並べる性質上、
AbcSize / MethodLength の閾値を超えやすい。慣例に倣い除外する。
「正常系」「DB 接続失敗時」だけだと cop が日本語前置を認識できないため、
明示的に when を先頭に付ける。
C1: yaml の id 採番と destroy_all 経路を廃止
- db/data/*.yml から id 列を削除、provider_id/plan_id を provider_name/plan_name に変更
- db/seeds.rb を find_or_create_by! ベースに全面書き換え (setval / destroy_all を削除)
- ElectricityBillSimulator の sort tiebreaker を [price, provider.name, plan.name] に変更

H1: Plan に料金算出責務を集約
- Plan#base_price_for(ampere) を追加 (空 → 0、該当あり → rate、該当なし → nil)
- Plan#metered_only? を削除し、Service の base_price 分岐を Plan に移動
- spec/models/plan_spec.rb で base_price_for の挙動を 3 ケース検証

M3: シードデータの不変条件を CI で保護
- spec/data_consistency_spec.rb に「各プラン最終段の kilowatt_hour_high >= MAX_KWH」を追加
- silent な料金過小算出のリスクをエラーメッセージに明記

副次変更:
- data-dependent test (Plan.count == 4, result.size == 4) を将来のプラン追加に耐える形に修正
- spec/models/usage_based_rate_spec.rb の id/plan_id ハードコードを factory ベースに
- describe 文の (Plan id=N) 表記を削除

検証:
- rspec: 75 examples, 0 failures (価格固定値テスト全件パス → ふるまい等価)
- db:seed 2 回実行で冪等性確認 (Provider: 3, Plan: 4, AmpereBasedRate: 18, UsageBasedRate: 10)
- API: ampere=30, kwh=400 で従来と同一の 4 プラン分レスポンス
- rubocop: no offenses

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
503 を返すヘルスチェック失敗は「人間の調査が必要」な事象であり、
warn ではなく error レベルで出すのが運用上の慣例 (CloudWatch アラート設計の根拠)。

rescue StandardError は意図的に広く捕捉している:
- Healthz は「アプリ全体の健全性」を 200/503 で表現するエンドポイント
- 想定外例外 (NoMethodError 等の実装バグ) も 503 で ALB から退避すべき
- この意図が普通のコントローラの常識と異なるため WHY コメントで明示

ログメッセージを "Healthz DB check failed" → "Healthz check failed" に変更:
- DB に限らない捕捉になっているため、文言を整合させる
- 関連テストも追随

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ActiveHash 経由から DB 管理に切り替えた前提変更を反映して、
spec の責務を再評価し redundant / データ依存テストを削除する。

削除:
- 「モデルバリデーション (全レコード valid)」 (4 examples)
  - seeds.rb の save! / update! / find_or_create_by! が validation 経由で
    例外を出すため、seed が成功した時点で自動保証される
  - ActiveHash 時代は yaml → オブジェクト変換時に validation が走らない
    可能性があったため必要だったが、DB 管理では redundant
- 「基本料金 0 円のプランが少なくとも 1 つ存在する」 (1 example)
  - データ依存テスト (新プラン構成で容易に壊れる)
  - base_price_for の挙動は plan_spec.rb で factory ベースで検証済み

残す (新プラン追加で編集不要な invariant のみ):
- 入力可能範囲のカバレッジ (kilowatt_hour_high >= MAX_KWH)
- 料金レンジの連続性 (最初の段 low=1 / 段の間に穴・重複なし)

いずれも「複数レコード間 / 全プラン横断」の invariant で、
DB の check_constraint や model validation では表現できない固有の検証価値。

検証:
- rspec: 70 examples, 0 failures (75 → 70、削除分 5 reflected)
- rubocop: no offenses

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ral-key

refactor(rails): マスターデータを natural key 方式に移行し料金算出責務を Plan に集約
…g-level

fix(healthz): log level を error に変更し rescue 範囲の WHY コメントを追加
@en-Barry en-Barry merged commit 9b89951 into master May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant