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