DEV Community

Linuxander
Linuxander

Posted on

Rails bitcoin prodavnica - deo II

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 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:

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
Enter fullscreen mode Exit fullscreen mode

Nakon toga otvorite OrdersController i dodajte sledeci kod:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

Otvorite PaymentsController i promenite kod da izgleda ovako:

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

Sada promenite Vas Payment model:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)