【Rails】furimaアプリ　商品購入機能　#1

# 実装概要
商品をクレジットカード決済で購入する機能を実装から単体テストまで行う

### 事前準備 Turbo機能をオフにする
```ruby:views/items/show.html.erb
# 中略

~data: { turbo: false }~%>

# 中略
```
PAY.JPを使用するためには、Turboの機能をオフにする必要があるため上記コードを追記

### Purchasesコントローラーの作成
ターミナル
```
% rails g controller purchases
```

### indexアクションの定義
```ruby:app/controllers/purchases_controller.rb
def index
end
```

### ルーティングの設定
```ruby:config/routes.rb
# 中略
resources :purchases, only: :index
```

### 購入、送付先住所についてのモデルとテーブルの作成
ターミナル
```
% rails g model purchese
% rails g model shipping_address
```

マイグレーションファイルを編集
```ruby:db/migrate/2024******_create_purchases.rb
class CreatePurchases < ActiveRecord::Migration[7.0]
  def change
    create_table :purchases do |t|
      t.references    :user, null: false, foreign_key: true
      t.references    :item, null: false, foreign_key: true
      t.timestamps
    end
  end
end
```
```ruby:db/migrate/2024*****_create_shipping_addresses.rb
class CreateShippingAddresses < ActiveRecord::Migration[7.0]
  def change
    create_table :shipping_addresses do |t|
      t.string      :postal_code, null: false
      t.integer     :prefecture_id, null: false
      t.string      :city, null: false
      t.string      :address, null: false
      t.string      :building_name
      t.string      :phone_number, null: false
      t.references  :purchase, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```
マイグレーションファイルを実行
ターミナル
```
% rails db:migrate
```

### アソシエーションの追加
```ruby:app/models/user.rb
# 中略
has_many :purchases
```
```ruby:app/models/item.rb
# 中略
has_one :purchase
```
```ruby:app/models/purchase.rb
belongs_to :user
belongs_to :item
has_one :shipping_address
```
```ruby:app/models/shipping_address.rb
belongs_to :purchase
```

### Formオブジェクトの作成
modelsディレクトリ直下にpurchase_form.rbファイルを作成
併せてバリデーションも設定
```ruby:app/models/purchase_form.rb
include ActiveModel::Model
attr_accessor :user_id, :item_id, :postal_code, :prefecture_id, :city, :address, :building_name, :phone_number
with_options presence: true do
    validates :user_id
    validates :item_id
    validates :postal_code, format: { with: /\A[0-9]{3}-[0-9]{4}\z/, message: 'is invalid. Include hyphen(-)' }
    validates :city
    validates :address
    validates :phone_number, format: { with: /\A[0-9]{10,11}\z/, message: 'must be 10 or 11 digits long' }
  end
  validates :prefecture_id, numericality: { other_than: 0, message: "can't be blank" }
```
`attr_accessor`は、インスタンス変数の読み書き用メソッドを自動生成し、コードをシンプルにするために使われる。これにより、メンテナンスが容易になり、コードの可読性も向上する。また、`attr_reader`や`attr_writer`で読み取り専用・書き込み専用の設定も可能。

`with_options`は、共通のバリデーションや設定をまとめて適用するために使用する。これにより、重複コードを減らし、可読性が向上される。Railsでのモデルのバリデーションやオプションの指定を簡潔に書く際に便利

### データベースに購入情報（Purchase）と配送先情報（ShippingAddress）を保存するための処理を記述。

```ruby:app/models/purchase_form.rb
# 中略
def save
    purchase = Purchase.create(user_id:, item_id:)

    ShippingAddress.create(
      postal_code:,
      prefecture_id:,
      city:,
      address:,
      building_name:,
      phone_number:,
      purchase_id: purchase.id
    )
  end
end
```

### 購入機能に必要なnewアクションとcreateアクションの定義
```ruby:app/controllers/purchases_controller.rb
# 中略

def new
    @purchase_form = PurchaseForm.new
  end

  def create
    @purchase_form = PurchaseForm.new(purchase_params)
    if @purchase_form.valid?
      @purchase_form.save
      redirect_to root_path
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def purchase_params
    params.require(:purchase_form).permit(:postal_code, :prefecture_id, :city, :address, :building_name,
                                          :phone_number).merge(user_id: current_user.id, item_id: params[:item_id])
  end
end
```

### ルーティングの修正
購入機能は、商品情報にネストされたルーティングの設定がされていなかったため修正。ネストすることで、`items`コントローラーで定義した変数を利用できるようになる。

```ruby:config/routes.rb
# 中略
resources :items do
    resources :purchases, only: [:index, :new, :create]
end
```

### indexアクション内のコード編集
```ruby:app/controllers/purchases_controller.rb
def index
    @item = Item.find(params[:item_id])
    @purchase_form = PurchaseForm.new
end
```
#### 1.商品の取得:

`@item = Item.find(params[:item_id])`の部分は、特定の商品情報をデータベースから取得する。`params[:item_id]`は、URLから取得した商品ID。このIDを使用して、該当する商品をデータベースから検索し、その情報を`@item`に格納する。
商品の詳細情報を表示するためには、正しい商品データを取得することが必須。この情報がないと、ユーザーに対して適切な商品情報を表示することができない。
#### 2.購入フォームの初期化:

`@purchase_form = PurchaseForm.new`の部分では、購入に必要な情報を入力するためのフォームオブジェクトを初期化している。これにより、購入に必要な情報（住所、電話番号など）を収集するためのフォームが準備される。
フォームオブジェクトを使用することで、入力内容の検証やデータの保存を簡単に行えるようになる。

### 購入のためのフォームを実装
```ruby:app/views/purchases/index.htmle.erb
# 中略
<%= form_with (model: @purchase_form, url: item_purchases_path(@item.id), id: 'charge-form', class: 'transaction-form-wrap',local: true) do |f| %>
# 中略
```

- `url: item_purchases_path(@item.id):` ここで `@item.id` を渡すことで、現在の商品に紐づいた購入が正しく処理される
- `model: @purchase_form: @purchase_form` はフォームオブジェクトのインスタンスであり、モデルの役割を果たす。これにより、バリデーションエラーメッセージが自動的に関連フィールドに表示される

この設定で、購入データが送信される際に指定した create アクションにリクエストが届き、正しいフォーム送信ができるようになる。

### 現時点までの実装でフォームの情報が保存されるのか確認
`Template is missing`エラーが発生

該当箇所を確認
```ruby:purchases_controller.rb
# 中略

render :new, status: :unprocessable_entity

# 中略
```

`new.html.erb`ファイルが存在していないのに`render :new`を指定してしまっていたためエラーが発生していた。
下記に修正
```ruby;purchases_controller.rb
render :index, status: :unprocessable_entity
```

続いて`NoMethodError`が発生。
`<%= image_tag @item.image, class: 'buy-item-img' %>`部分が該当していた。
最初に商品購入ページへ遷移した際は、問題なく表示がされていたので、再表示される際に何かしらの問題が発生していると予想し、`create`アクション内を確認。
すると、`@item`が再設定されていないことが確認できた。
下記にコード修正
```ruby:app/controllers/purchases_controller.rb
# 中略

def create
  @item = Item.find(params[:item_id])
  @purchase_form = PurchaseForm.new(purchase_params)
  if @purchase_form.valid?
    @purchase_form.save
    redirect_to root_path
  else
    render :index, status: :unprocessable_entity
  end
end
```

ここで`new`アクションと`create`アクションで`@item = Item.find(params[:item_id])`が共通して使用されていたため、`private`メソッド内に`set_item`メソッドを定義し、`before_action`で呼び出せるように修正
`※コード記載は省略。`

### テストコードを実行する
ここまでの実装が正しいかをテストコードを用いて確認

次回はPAY.JPを利用してクレジットカード決済ができるように実装を進めていきます！