Rails bitcoin prodavnica - deo II
Tags: ruby, rails, blockchain, webdev
Published: 2020-12-14 11:59:16

*Part II of "Shopping Cart with Bitcoin payout" tutorial in Serbian language. I will create a full application with tutorial in English for Hackathon. I will also add nested-attributes for order-payment and more checkout options.*

---

U [prvom delu tutoriala](https://dev.to/linuxander/rails-bitcoin-prodavnica-deo-i-52ph) napravili smo korisnike, proizvode, i korpu. U ovom delu kreiracemo porudzbine iz korpe, i procesuirati uplatu na bitcoin adresu.

---

## Porudzbine

---

Nastavljamo sa scaffold-om, kao i u [prvom delu](https://dev.to/linuxander/https://dev.to/linuxander/rails-bitcoin-prodavnica-deo-i-52ph):

```bash
rails g scaffold order cart_id:integer, user_id:integer, full_price:decimal, cart_price:decimal, shipping_price:decimal, country, address, canceled:boolean, complete:boolean, status, token
```  

Nakon toga otvorite *OrdersController* i dodajte sledeci kod:

```ruby
class OrdersController < ApplicationController

  before_action :authenticate_user!
  before_action :set_cart,  only: [:create]
  before_action :set_order, only: [:show, :destroy, :delivered]


  def index
    @orders = current_user.orders.recent.last(30)
  end

  def show
  end

  def new
    @order = current_user.orders.build
  end

  def create

    country = params[:order][:country]  ||= current_user.country
    address = params[:order][:address]  ||= current_user.address

    unless cart_empty?

      @order = current_user.orders.build(order_params)
      @order.construct_from_cart(current_cart, country, address)

      if @order.save; @cart.destroy
        redirect_to @order, notice: "Order: #{@order.address} has been created. Charity donation: $#{@order.charity_fee}."
      else
        redirect_to @cart, notice: "An error ocurred with your order!"
      end

    else
      redirect_to products_path, notice: "Your Cart is empty."
    end
  end

  def destroy
    if @payment.status == 'unpaid'
      @payment.cancel! and @order.cancel!
      redirect_to orders_path, notice: "Order #{@order.token} has been canceled!"
    else
      redirect_to @order, notice: "Order can't be canceled at this step!"
    end
  end

  def delivered
    if @payment.status == 'complete'; @order.finalize!
      redirect_to @order, notice: "Order: #{@order.token} has been delivered!"
    else
      redirect_to @order, notice: "Please finalize your payment before marking order as delivered."
    end
  end


  private

  def set_order
    @order   = current_user.orders.find(params[:id])
    @payment = Payment.find_by(order_id: @order.id)
  end

  def cart_empty?
    current_cart.cart_items.count < 1
  end

  def order_params
    params.require(:order).permit(:cart_id, :user_id, :full_price, :cart_price, :shipping_price, :site_fee, :country, :address, :description, :canceled, :complete, :status, :token)
  end

end
```  

Metod *create* koristi `:construct_from_cart`, metod koji smo definisali u modelu. Otvoride model **Order** i dodajte sledece:

```ruby
class Order < ApplicationRecord

  belongs_to :user
  has_one    :cart
  has_many   :payments, dependent: :destroy

  scope      :recent, -> { order created_at: :desc }

  validates  :user_id, :cart_id, :full_price, :country, :address, :token, :status, presence: true


  def construct_from_cart(cart, country, address)
    self.cart_id        = cart.id
    self.token          = cart.token
    self.cart_price     = cart.total_price
    self.shipping_price = get_shipping_price(country)
    self.country        = country
    self.address        = address
    self.status         = 'created'
    self.description    = desc = ""

    current_cart.cart_items.each do |item|
      desc << "#{item.quantity} x #{item.product.title}; "
    end
  end

  def get_shipping_price(country)
    ShippingPrice.for(country)
  end

  def site_fee
    #define your site fee as ENV variable
  end

  def full_price
    self.cart_price + self.site_fee + self.shipping_price
  end

  def cancel!
    self.canceled = true
  end

  def finalize!
    self.complete = true
  end

end
```  

Ovde smo napravili metod koji smo koristili u kontroleru, koji popunjava tabelu *Orders* uz pomoc zadatih podataka.
Podaci neophodni za kreiranje porudzbine su korpa, drzava i adresa. Kod je prilicno samo-objasnjavajuci, sto je jedna
od dobrih odlika Ruby-ja.

Ono sto nije je objasnjeno je odakle dolazi **Shipping#price** metod? U folderu **concerns** u modelima, gde smo definisali
nasu korpu *(shopping_cart.rb),* sada kreirajte novi dokument i nazovite ga `shipping_price.rb`. U njega dodajte sledeci kod:


```ruby
module ShippingPrice

  COUNTRIES = %w{ SERBIA GREECE RUSSIA EUROPE WORLDWIDE }

  private

  def self.for(country)

    case country
      when 'SERBIA'    then 5
      when 'GREECE'    then 15
      when 'RUSSIA'    then 30
      when 'EUROPE'    then 20
      when 'WORLDWIDE' then 35
    end    

  end
end
```  

Ovde imamo definisan modul koji koristimo u porudzbinama, *ShippingPrice.for(country)*, kao i kolekciju **COUNTRIES**
koju koristimo da odredimo cene za odredene zemlje. Kolekciju *countries* cemo takodje koristiti u *views (.html.erb)* fajlovima kao izor drzave.
Za sve ovo postoji mnogo bolji nacin, ali zeleo sam da to uradim brzo i jednostavno, bez svih drzava, dodatnih tabela itd... Na kraju nam ostaju migracije:

```ruby
class CreateOrders < ActiveRecord::Migration[6.0]
  def change
    create_table :orders do |t|
      t.integer  :user_id, null:false, foreign_key: true
      t.integer  :cart_id
      t.string   :country
      t.text     :address
      t.text     :description
      t.decimal  :cart_price
      t.decimal  :shipping_price
      t.decimal  :full_price
      t.decimal  :site_fee
      t.string   :token
      t.string   :status
      t.boolean  :canceled
      t.boolean  :complete

      t.timestamps
    end
  end
end
```
Sada kada su nam porudzbne funkcionalne, predjmo na placanje, kako bi smo popunili *@payments* variable u porudzbinama.

---

## Procesuiranje Porudzbine

---

Napravite novi *scaffold* za uplate, odnsno procesuiranje istih:

```bash
rails g scaffold payment price:decimal, order_id:integer, cart_id:integer, user_id:integer, address, public_key, balance:decimal, index:integer, token, canceled:boolean, complete:boolean, status
```

Otvorite *PaymentsController* i promenite kod da izgleda ovako:


```ruby
class PaymentsController < ApplicationController

  include Bitcoin

  before_action :authenticate_user!
  before_action :get_payments, except: [:create]
  before_action :set_status,   only:   [:index]
  before_action :set_payment,  only:   [:show, :destroy]


  def index
    @payments = @user_payments.recent.last(30)
  end

  def show
  end

  def new
    @payment = current_user.payments.build
  end

  def create

    generate_payment_address!
    @order   = Order.find(params[:order_id])
    @payment = current_user.payments.build(payment_params)
    @payment.construct_from_order(@order, @index, @bip32, @address)

    if @payment.save
      redirect_to @payment, notice: "To finalize order, send $#{@payment.price} to address: #{@payment.address}"
    elsif @order
      redirect_to @order, notice: "An error ocurred while creating payment. Please try again."
    else
      redirect_to root_path, notice: "An error ocurred - wrong Order ID."
    end
  end

  def destroy
    if @payment.status == 'unpaid' 
      @payment.cancel! and @order.cancel!
      redirect_to payments_url, notice: "Payment for Order: #{@order.token} was successfully canceled."
    else
      redirect_to @payment, notice: "Order can't be stopped at this step!"
    end
  end


  private

  def get_payments
    @user_payments = Payment.where(user_id: current_user.id, canceled: false).find_each
  end

  def set_payment
    @payment = @user_payments.find(params[:id])
    @order   = orders.find(@payment.order_id)
    @payment.update! unless @payment.complete or @payment.canceled
  end

  def set_status
    payments = @user_payments.recent.last(5)
    payments.each { |p| p.update! unless (p.complete or p.canceled) }
  end

  def generate_payment_address!
    payment  =  Payment.last
    payment  ?  index = payment.index  :  index = 0
    wallet   =  generate_new_address_from(index)
    @index   =  wallet.index
    @bip32   =  wallet.to_bip32
    @address =  wallet.to_address
  end

  def payment_params
    params.require(:payment).permit(:user_id, :order_id, :price, :balance, :public_key, :address, :index,
                                    :status,  :canceled, :complete, :token)
  end

end
```

Ovde smo uradili metod `:generate_payment_address` da kreira novu adresu za svaku uplatu. Za to smo takodje koristili **concerns** u modelima, i napravili novi `bip32.rb`.


```ruby
module Bip32

  private

  #   key should be defined as ENV config variable!
  KEY = 'xpub6AvUGrnEpfvJJFCTd6q****************************cfBUbeUEgNYCCP4omxULbNaRr'
  API = BlockCypher::Api.new

  def generate_new_address_from(index)
    wallet = initialize_wallet_node!
    depth  = wallet.depth
    path   = "M/#{depth}/#{index += 1}"
    pubkey = wallet.node_for_path path
    return pubkey if pubkey
  end

  def initialize_wallet_node!
    MoneyTree::Node.from_bip32(KEY)
  end

  def get_address_data(address)
    balance = API.address_balance(address)
    return balance if balance
  end

  def payment_success?(address, price)
    data = get_address_data(address)
    data ? coins = data[:balance] : coins = 0
    return true unless coins < price
  end

end
```  

Sada promenite Vas *Payment* model:


```ruby
class Payment < ApplicationRecord

  belongs_to :order
  belongs_to :user

  scope     :recent, -> { payment created_at: :desc }

  validates :order_id, :user_id, :price, :address, :index, :public_key, :status, :balance, presence: true


  def update!

    bip32 = Bip32.get_address_data(self.address)
    data  = JSON.parse(bip32)

    unless data.nil?

      balance    = data[:balance]
      no_balance = data[:unconfirmed_balance]

      if no_balance == payment.price
        update_status('waiting payment', 'waiting confirmations')

      elsif no_balance > 0 and balance == 0
        update_status('waiting payment', 'processing payment') unless no_balance == payment.price

      elsif balance > 0 and balance < payment.price
        update_status('processing payment', 'underpaid')

      elsif balance == payment.price or balance > payment.price
        update_status('waiting delivery', 'complete') and finalize!
      end
    end
  end

  def construct_from_order(order, index, bip32, address)
    self.user_id    = order.user_id
    self.order_id   = order.id
    self.token      = order.token
    self.price      = order.full_price
    self.index      = index
    self.public_key = bip32
    self.address    = address
  end

  def update_status(order_status, payment_status)
    new = Order.find(self.order_id)
    new.status  = order_status
    self.status = payment_status
  end

  def cancel!
    self.canceled == true
  end

  def finalize!
    self.complete == true
  end

end
```  

Ovo je skoro identicno porudzbinama, i takodje je sve samo-objasnjavajuce. Popunite migracije i izvrsite `rails db:migrate`:

```ruby
class CreatePayments < ActiveRecord::Migration[6.0]
  def change
    create_table :payments do |t|
      t.integer :order_id,  foreign_key: true
      t.integer :user_id,   foreign_key: true
      t.integer :cart_id
      t.decimal :price
      t.decimal :balance,   null: false, default: 0
      t.string  :public_key
      t.string  :address
      t.integer :index
      t.string  :status,    null: false, default: 'unpaid'
      t.boolean :canceled
      t.boolean :complete

      t.timestamps
    end
  end
end
```  

Sada imamo dodate porudzbine i uplate za njih. Obzirom da koristimo samo jedan nacin uplate, @payment.cancel! automatski prekida i porudzbinu.
  
> U trecem delu cemo uraditi izgled i rute, iako je vecina njih dodato automatski uz scaffold, treba ih delimicno izmeniti. Osim toga treba izmeniti *ApplicationController*, is jos nekoliko stvari koje cu opisati u trecem delu. Sve u svemu, potpuno funkcionalna prodavnica, uz mogucnost lakog ubacivanja Stripe-a ili nekog drugog payout sistema.