ASTRO Camp Day33 - RUBY(11)
RUBY 第十一堂課
前情提要
最後再來處理checkout的畫面了,新增一個checkout.erb.html
我們準備要把刷卡機放在這個畫面
1、Paypal金流練習
braintree官網
先到上面網址註冊一個帳號,記得要註冊sandbox的,他是給你測試用的,註冊好後去信箱收信,從連結登入就可以
SDK是什麼 - 軟體開發套件(不單指一個)
串金流需要用到的是客戶端和server端的SDK
Client SDKs - for Android, iOS, and JavaScript
Server SDKs - for Java, .NET, Node.js, PHP, Python, and Ruby
1-1、BrainTree刷卡的流程圖
- 一開始,用戶使用手機或電腦,打開你的網站的時候,他們會給我們一個request,我們會給他一個response(以此次做的網頁為例,他們看到的網頁連結就是”/orders/訂單編號/checkout”)
- 在用戶瀏覽此網頁的時候,Server經過驗算,會偷偷塞一個token給用戶(不是rails給的喔)
- 當使用者輸入信用卡,按下送出之後,他不會馬上送回我們的Server,會先送到BrainServer,再來BrainServer會給使用者一個Nonce
- 用戶收到Nonce(隨機數)後,會把Nonce送到我們的Server這邊
- 最後我們拿著使用者給我的Nonce,去跟BrainServer做交易
nonce是什麼?他就是只能用一次的隨機數字
英文縮寫是number only use once
正常來說網站不會存使用者的完整信用卡卡號
如果在台灣如果你要在你的server存取信用卡卡號,要符合pci compliance的規定
1-2、Set Up Your Client
來處理JS前端的部分,首先增加Drop-in到你的頁面
> <div id="dropin-container"></div>
設好Drop-in後,並且用promises的方式來抓他
> braintree.dropin.create({
> container: document.getElementById('dropin-container'), # container放上面那個div區塊
> }).then((dropinInstance) => { # 猜測這個是上面拋下來的東西
> }).catch((error) => {});
1-3、Get a client token
JavaScript SDK需要一個由您的BrainTree服務器所產生的Client Token,一旦你產生了客戶端Token,請你放到範例中authorization的位置
braintree.dropin.create({
authorization: CLIENT_TOKEN_FROM_SERVER, # 這個就是客戶端token
container: '#dropin-container' # 這個是剛剛的dropin-container
}).then((dropinInstance) => {
}).catch((error) => {});
該換我們操作了,在checkout建立一個dropin,等等要來操作他
> checkout.html.erb
>
> <h2>結帳</h2>
>
> <div id="dropin" data-controller="paypal-dropin"></div> # 增加dropin
並建立新的paypal_dropin_controller,這個controller可以操作剛剛那個div,首先把基本的建立好,並隨便印出一個數讓他可以動
> import { Controller } from "stimulus"
> export default class extends Controller {
> static targets = [ ]
>
> initialize() { }
> connect() {
> console.log("123");
> }
> }
1-4、BrainTree的npm/yarn
我們來使用npm的方法來抓金流,先不要用cdn的方法 BrainTree的npm
> yarn add BrainTree-web-drop-in
輸入完指令後,先出找到module檔案裡,BrainTree的名字,接著import先隨便試,因為不知道對方怎麼export的,所以那邊名字隨便打都可以,如果這樣不行的話,再加上大括號試試(這種引入猜測很常用到喔!!)
> paypal_dropin.controller
> import { Controller } from "stimulus"
> import dropin from "braintree-web-drop-in" # braintree是隨便打的,猜測他們有default export
>
>
> export default class extends Controller {
> static targets = [ ]
>
> initialize() { }
> connect() {
> console.log("123");
> console.log(dropin); # 這樣印就可以印出braintree那一包
> } 仔細看會發現裡面會有剛剛看到的create
>
> }
確定有掛上controller後,可以開始寫dropin了,這邊只寫connect(),上下都省略
> paypal_dropin.controller
> connect() {
> drop.create({
> container: this.element, # 剛剛文件是是用getElement,我們直接用this.element就抓到了
> authorization: "123" # 這個先隨便給,目前一定會錯,之後再填上
> })
> .then((instance) => {
> console.log(instance)
> })
> .catch((err) => {
> console.log(err)
> })
> }
1-5、產生authorization token
要產出這出這個token,需要三個金鑰,這個金鑰金流商會給你,下面是示範用,所以我直接列出來,等等我們就可以用三個key轉出一大串的亂碼
> 下面這一串是金流商直接提供給我們的商店key,等等就可以用這個產出token
> gateway = Braintree::Gateway.new(
> :environment => :sandbox,
> :merchant_id => 'qd67XXXXXXXx63',
> :public_key => '8d6XXXXXXXX32t',
> :private_key => '8fd0051cXXXXXXXXXX64339cb',
> )
1-6、Send payment method nonce to server
BrainTree透過收集客戶端傳來的信用卡資訊,把這些東西轉成nonce(只能用一次的隨機數),接下來下面是一個form表單,為啥突然變表單呢?剛剛不是只是一個div嗎?原因就是轉成nonce後,要再送到自己的網站,之後再從你的網站傳給金流商做比對
所以接下來的form,就是在做轉成nonce後要再送到自己的網站
> 文件資訊
> <form id="payment-form" action="/route/on/your/server" method="post"> # 送出後轉nonce再傳給自己
> <!-- Putting the empty container you plan to pass to
> `braintree.dropin.create` inside a form will make layout and flow
> easier to manage -->
> <div id="dropin-container"></div> # 剛剛的controller在這
> <input type="submit" />
> <input type="hidden" id="nonce" name="payment_method_nonce"/>
> </form>
文件資訊,範例建好表單,再操作controller,這邊的行為在流程圖就是3、4的那幾條線
> const form = document.getElementById('payment-form'); # 抓到上面的form
>
> braintree.dropin.create({
> authorization: 'CLIENT_AUTHORIZATION',
> container: '#dropin-container'
> }).then((dropinInstance) => {
> form.addEventListener('submit', (event) => { # 幫表單加監聽器
> event.preventDefault(); # 把表單預設行為停下來
>
> dropinInstance.requestPaymentMethod().then((payload) => { # 抓一包東西回來(nonce)
> document.getElementById('nonce').value = payload.nonce; # 把剛剛拿到的東西,塞進上面隱藏的input
> form.submit(); # 表單送出
> }).catch((error) => { throw error; });
> });
> }).catch((error) => {
> // handle errors
> });
Ps. 上面有一段requestPaymentMethod,看不懂沒關係,他就是把nonce傳給我們
剛剛要把表單預設行為停下來的原因,就是因為我們要先得到nonce,再手動提交,要不然拿不到nonce
1-7、Set Up Your Server
接下來我們要來處理Server端了,首先要處理的就是前面說的(2),要把token塞給使用者,不過實作前,我們先載入braintree提供的gem
> gem "braintree", "~> 4.9.0"
重新bundle
> bundle
安裝好後開始實作拉,我們到結帳頁面(/orders/訂單編號/checkout),還記得剛剛說的嗎?客戶到這個頁面的時候,我們要偷偷塞一個token給他,這個行為要在哪裡做呢?就是在order controller做
> orders_controller.rb
> private
> def gateway # 把系統商給的方法,貼到private這邊
> gateway = Braintree::Gateway.new( 並把自己商店的key填上
> :environment => :sandbox, 等等呼叫gateway會回傳下面這些東西
> :merchant_id => 'qd67XXXXXXXx63',
> :public_key => '8d6XXXXXXXX32t',
> :private_key => '8fd0051cXXXXXXXXXX64339cb',
> )
> end
1-8、Generate a client token
文件說明:照著他寫,就可以產生一段token
(1) gateway是剛剛寫的私有方法
(2) client_token、generate是他提供的
(3) customer_id是客戶的id,不過你不帶也沒關係
> @client_token = gateway.client_token.generate( # 產生token
> :customer_id => a_customer_id # 可寫可不寫
> )
看完文件來寫我們的產生token方法
> orders_controller.rb
> def checkout
> @token = gateway.client_token.generate # 這個都是照個手冊寫的
> end
> private
> def gateway # 把系統商給的方法,貼到private這邊
> gateway = Braintree::Gateway.new( 並把自己商店的key填上
> :environment => :sandbox, 等等呼叫gateway會回傳下面這些東西
> :merchant_id => 'qd67XXXXXXXx63',
> :public_key => '8d6XXXXXXXX32t',
> :private_key => '8fd0051cXXXXXXXXXX64339cb',
> )
> end
如果這時候你把這個token印出來,會發現超級長的一串亂碼
> checkout.html.erb
>
> <%= @token %> # 超長一段亂碼
> <h2>結帳</h2>
>
> <div id="dropin" data-controller="paypal-dropin"></div> # 增加dropin
確定token有產生出來後,我們要來把token傳去JS那邊了,為啥要傳去那邊呢?還記得剛剛有一段authorization嗎?忘了沒關係,等等會看到,不過要怎麼帶過去呢?這時候又要使用data屬性了!使用data屬性就可以把ruby傳給JS
> checkout.html.erb
>
> <h2>結帳</h2>
>
> <div id="dropin" data-controller="paypal-dropin" data-token="<%= @token %>"></div> # 增加dropin
這邊設定好就可以到controller那邊抓了,把token塞進剛剛說的authorization後,刷卡機就會跑出來了!!!
> paypal_dropin.controller
> connect() {
> const token = this.element.dataset.token # 抓到那個token
>
> drop.create({
> container: this.element,
> authorization: token # 把token塞進去
> })
> .then((instance) => {
> console.log(instance)
> })
> .catch((err) => {
> console.log(err)
> })
> }
測試號碼
visa給的測試卡號 第一個是4、後面全都是1(15個1)
日期可以隨便填
1-9、建立form傳送token,並得到nonce
再來要做一個form給刷卡機,我們要做一個路徑接收剛剛傳過來的東西
> 先想一下路徑: POST /order/訂單編號/pay
> 因此到routes把路徑加上
> resources :orders, only: [:create] do
> member do
> get :checkout
> post :pay # 這一條!!!
> end
> end
路徑做好來做表單拉~
表單注意事項
- id記得要給我們轉成亂數過後的訂單邊到喔
- 這個表單不用model
- @order是在order controller的checkout那邊傳過來的
> @order = Order.find_by!(serial: params[:id])
完整表單
> checkout.html.erb
>
> <h2>結帳</h2>
> <%= form_with url: pay_order_path(id: @order.serial) do |end| %>
> <div id="dropin" data-controller="paypal-dropin" data-token="<%= @token %>"></div>
> <% end %>
再來我們要來停下表單的預設事件了,不過還記得為什麼要停下表單預設事件嗎?原因就是收到nonce的流程是這樣
(1) 這個form表單會傳出token給點擊送出表單的使用者
(2) 此時要先停止預設事件,要先到金流商那邊取得nonce
(3) 取得nonce後,最後再手動submit,這樣我們就可以從使用者(金流商)那邊取得nonce了
不過目前我們的表單設計,因為我們的controller是掛在input上,所以無法抓到表單,所以為了不用再多創一個controller,我們直接把input的controller拿掉,移到form上面,順便把token帶過去
> checkout.html.erb
>
> <h2>結帳</h2>
> <%= form_with url: pay_order_path(id: @order.serial), data: {controller: "paypal-dropin",
> token: <%= @token %>, }
> do |end| %>
>
> <% end %>
不過這樣寫會遇到另外一個問題,因為這樣會噴錯誤說,controller must reference an empty DOM node,所以我們要塞一個DOM給他,就直接塞一個簡單的div就好了
> paypal_dropin.controller
> connect() {
> const token = this.element.dataset.token
> const div = document.createElement("div") # 創一個div的區塊
> this.element.appendChild(div) # 把他用appendChild塞進form裡面
>
> drop.create({
> container: div, # container記得要改成div
> authorization: token # 把token塞進去
})
.then((instance) => {
console.log(instance)
})
.catch((err) => {
console.log(err)
})
}
照這樣改完之後,就可以讓刷卡機跑出來了!!不過這樣還沒完成,還記得剛剛說的暫停預設行為嗎,現在來寫這一塊
> paypal_dropin.controller
connect() {
const token = this.element.dataset.token
const div = document.createElement("div")
this.element.appendChild(div)
drop.create({
container: div,
authorization: token
})
> .then((instance) => { # 這一段開始都是跟著文件做的
> this.element.addEventListener("submit", (e) => {
> e.preventDefault()
> instance.requestPaymentMethod()
> .then((preload) => {
> console.log(preload) # 印出preload,會發現有一包信用卡相關訊息,nonce就在裡面
> }).catch((error) => { error })
> })
>
> })
> .catch((err) => {
> console.log(err)
> })
> }
知道nonce在哪之後,就可以把它從preload那一包解構出來了
> paypal_dropin.controller
drop.create({
container: div,
authorization: token
})
.then((instance) => {
this.element.addEventListener("submit", (e) => {
e.preventDefault()
instance.requestPaymentMethod()
> .then(({nonce}) => {
> this.setNonce(nonce) # 這邊把nonce值帶進去
> this.element.submit() # 手動把表單提交
> }).catch((error) => { error })
> })
})
.catch((err) => {
console.log(err)
})
}
>
> setNonce(value) { # 這裡多做一個fc是因為,我們要多創一個input欄位
> const el = document.createElement("input") 等等想把nonce塞進來
> el.setAttribute("type", "hidden") # 下面三行是在設定input的欄位屬性
> el.setAttribute("name", "nonce")
> el.setAttribute("value", value) # 這一行就是把nonce值塞進來的地方
> this.element.appendChild(el) 把輸入欄位塞進form欄位
> }
這樣就可以確定收到nonce了,接下來我們把東西印出來看看!不過還記得我們剛剛form提交後,會去到哪嗎~要記得我們剛剛有多設定pay的路徑喔,所以要到order的controller新設定一個action
> order_controller.rb
> def pay
> render html: params # 我們把form傳來的那一包表單印出來
> end 會發現裡面有一堆東西,包括nonce、訂單編號
1-10、Receive a payment method nonce from your client
確定有nonce後,我們看一下文件,他怎麼處理的。他先宣告一個區域變數 = 抓取剛剛放置nonce的input欄位(記得這個input剛剛name是你自己取的喔,不要亂抄文件)
> post "/checkout" do
> nonce_from_the_client = params[:payment_method_nonce]
> end
1-11、Create a transaction
再來的文件是,把取得的東西,變成一張訂單
(1) gateway是剛剛建立的方法
(2) amount是order的欄位
(3) payment_method_nonce這個是剛剛拿到的nonce
(4) 後面兩個都不填沒關係,device那個主要是判斷客戶的裝置
> result = gateway.transaction.sale(
> :amount => "10.00",
> :payment_method_nonce => nonce_from_the_client,
> :device_data => device_data_from_the_client,
> :options => {
> :submit_for_settlement => true
> }
> )
1-12、Successful result
確認是否有成功完成訂單,使用這個方法會回傳true or false值
> result.success?
> #=> true
1-13、正式寫pay action
確認文件好,可以開始寫自己的訂單了
> order_controller.rb
> def pay
> @order = Order.find_by!(serial: params[:id]) # 用剛剛的訂單編號來抓訂單id
>
> result = gateway.transaction.sale(
> amount = @order.amount, # 訂單的價錢
> payment_method_nonce = params[:nance], # 抓到拿到的nonce
> )
>
> if result.success? # 這個方法是官方提供的
> // 改變訂單狀態
> redirect_to root_path, notice: "付款成功"
> else
> redirect_to root_path, alert: "付款發生問題"
> end
>
> end
最後最後,要記得把金鑰用環境變數藏起來,首先把原本放key的地方都用環境變數包起來
> order_controller.rb
> def gateway
> gateway = Braintree::Gateway.new(
> :environment => :sandbox,
> :merchant_id => ENV["BRAIN_MERCHANT_ID"],
> :public_key => ENV["BRAIN_PUBLIC_KEY"],
> :private_key => ENV["BRAIN_PRIVATE_KEY"],
> )
> end
1-14、新增環境變數
再來到.env_development的檔案,把key放上去
> .env_development
> BRAIN_MERCHANT_ID=qd67XXXXXXXx63
> BRAIN_PUBLIC_KEY=8d6yqXXXXXXXX2t
> BRAIN_PRIVATE_KEY=8fd0051cXXXXXXXX4339cb
設定好環境變數,記得要重新開server一次喔~
記得推上去前,要建一個.env.sample的檔案,裡面放的是之後給別人改成.env的資料
> .env.sample
> BRAIN_MERCHANT_ID=
> BRAIN_PUBLIC_KEY=
> BRAIN_PRIVATE_KEY=
也要記得把.env.development放進gitignore喔,先前有放進去過了,不過還是再提醒一次
2、拉下GitHub更改環境變數示範
龍哥試一次抓github的專案
> (1) git clone下來
> (2) bundle
> (3) yarn install
> (4) 把.env.example改成.env
> (5) 把自己商家的key填上env檔案
3、改變訂單狀態
有限狀態機是什麼!?狀態機的概念是說,打開狀態只能關閉 關閉狀態只打開
為什麼要引入狀態機?有了狀態機,就可以引入很多狀態,以訂單來說,我們就可以幫她設定pending、cancelled之類的
–
狀態機的意思就是會引入很多狀態,每個狀態間都會有一個動詞
狀態機就是一堆的if/else
–
3-1、狀態機推薦使用AASM
安裝AASM AASM GitHub
> gem 'aasm'
> bundle
3-2、狀態流程圖
把訂單所有狀態的傳遞都畫出來
> refund(動詞)
> refunded <--------
> | |
> | cancel |
> cancel(動詞) V (動詞) |
> ------- > cancelled |
> pending |
> ------- > paid ------------
> pay(動詞)
上面狀態圖的意思是
- pending(待付款)是一個狀態,經由兩個動詞,一個是pay,pay完後會到paid(已付款)的這個狀態
- paid這個狀態接後面的動詞為refund,後面的狀態為refunded(退款完成)
- refunded(退款完成)後面的動詞為cancel,也就是可以對退款進行取消這個動詞,最後是cancelled(取消)狀態
- 回到剛剛第一條,pending也可以經由cancel這個動詞,到cancelled這個狀態
畫完圖後,就來實際寫一次吧!首先到Order的Model,由於原本model還有其他東西,這次只顯示狀態機的書寫
3-3、狀態機設定
這邊有幾件事情要注意
- aasm column: “state”,這句的意思是,你指定Order的state欄位,設定為狀態機,這樣就可以啟用了
- 加上no_direct_assignment的意思是,讓訂單的狀態不能被強制更改,比如現在是pending狀態,你直接進資料庫改他狀態
- initial的意思是,你要讓整個訂單的初始狀態是什麼
- 記得要引入AASM才能用
> class Order < ApplicationRecord
>
> include AASM # 首先引入AASM
>
> aasm column: "state", no_direct_assignment: true do
> state :pending, initial: true
> state :paid, :cancelled, :refunded
>
> # 付款動作,是從待付款,到付款
> event :pay do
> transitions from: :pending, to: :paid
> end
> # 取消動作,是從待付款、退款,到取消付款
> event :cancel do
> transitions from: [:pending, :refunded], to: :cancelled
> end
> # 退款動作,是從待付款,到退款
> event :refund do
> transitions from: :paid, to: :refunded
> end
> end
> end
照上面這樣設定好後,就可以到console做一些有趣的事情
> rails c --sandbox # 進沙盒環境試試
> o1 = Order.first # 先抓第一筆訂單
3-4、來測試AASM送給你的一些方法
> o1.pending? # true -> 確認現在是否為"某狀態"
> o1.paid? # false
> o1.may_pay? # true -> 確定現在狀態下接下來可以做某些事情嗎(做動作)
> o1.may_refund? # false -> 因為pending狀態下,沒有接到refund動作
> o1.pay! # 這樣會直接把pending的狀態改成pay
> o1.refund! # 報一大堆錯,因為pending沒有接到refund的動作
如過今天你已經是paid的狀態
再執行o1.pay!,會直接報錯誤給你喔!!
3-5、完成pay action
這樣設定好後,我們就可以來把剛剛pay的改變訂單狀態寫完了
> order_controller.rb
> def pay
> order = Order.find_by!(serial: params[:id])
>
> if order.may_pay? # 先確定能不能付款
> result = gateway.transaction.sale(
> amount = @order.amount,
> payment_method_nonce = params[:nance],
> )
>
> if result.success?
> // 改變訂單狀態
> order.pay! # 能付款的話,直接把pending改成paid
> redirect_to root_path, notice: "付款成功"
> else
> redirect_to root_path, alert: "付款發生問題"
> end
> else
> redirect_to root_path, alert: "無效訂單" # 假設不能把狀態改成pay,回傳無效訂單
> end
> end
3-6、最後整理一下狀態機
fsm有一個重點,就是“有限“狀態機,不是無限的,無限是啥意思呢!?無限狀態就是指你可以亂設定,如果今天你沒有設定有限狀態機,就像你開手排車,可以直接從1檔打到8檔!
我們是經由操作他的動作,去改變它的狀態,而不是直接改變它的狀態
ex. 我是用關門這個動作,來讓打開的門關閉,而不是啥都不做原本打開的門,門就關起來了
有限狀態機不只可以用在model身上,其他情況也是可以用的,只是這裡剛好很適合用到在model上
4、Eslint
這個可以幫助整理你JS的檔案,比如說縮排不好,或是函式太多行之類的,他都會提醒你
> npm install eslint --global
接下來就可以執行eslint這個程式
> eslint 123.js # 後面是檔案名稱
不過應該會要你執行eslint的初始設定,他會問你一些問題
> npm init # 先生一個package.json檔案出來
> npm init @eslint/config # 再執行初始化
照他的規矩都寫好後,就可以再執行一次
> eslint 123.js # 後面是檔案名稱
5、rubocop
A Ruby static code analyzer and formatter, based on the community Ruby style guide.
可以整理你ruby的檔案,並依照你的情況幫你做修改、限制
5-1、安裝rubocop-rails
A RuboCop extension focused on enforcing Rails best practices and coding conventions.
專門整理rails檔案的插件
接下來我們就實際來安裝看看,把下面這一段放到gemfile
require: false的意思是,預設先關閉,我們呼叫他再出現就好
Ps. 放在gemfile的測試區域
> gemfile
> gem 'rubocop-rails', require: false
> bundle
5-2、啟動rubocop
安裝好後可以來啟動他了
接著它會噴一大串錯誤
> rubocop
5-3、強制修改
可以直接修改一大堆檔案
> rubocop -A # 強制修改可以改的檔案
5-4、新增規則
在做專案的時候,先把規矩都訂好,那要把這些規格放哪?
可以新增一個.rubocop.yml檔案
可以觀看這個網頁,有一些規格設定的方式
在檔案裡面新增這個,把預設的style/Document關掉
> Style/Documentation:
> Enabled: false
想忽略某些檔案怎麼辦,這邊直接把schema檔案忽略、bin相關檔案忽略
> Style/Documentation:
> Enabled: false
> AllCops:
> Exclude:
> - '/db/schema.rb'
> - 'bin/*'
> Metrics/MethodLength: 把最大行數加到20
> max: 20
Assignment Branch Condition 這個意思是,你的code寫太複雜了
ex. 你if判斷裡面又包了一層if
還可以在yml檔案裡面加上一段,可以把程式給的建議關掉
> AllCops:
> SuggestExtensions: false
最後把新警察加上去 Ps. 把新警察打開,會有其他錯誤跑出來,不過只要把它給解決掉,這個專案的coding style檢查就完成了
> AllCops:
> NewCops: enable
6、RSpec跑測試
bundle init可以建出一個空的gemfile
Ruby兩個測試工具比較
(1) mini-test - ruby內建,比較快
(2) rspec - 另外安裝,比較慢一點 -> 我們用這個寫
6-1、安裝rspec
首先下這個指令,就可以開始啟用RSpec
> bundle add rspec
測試就是在建立規格,先把規格訂好,再來寫code 建立新的檔案,並開始跑
> bank_spec.rb
> rspec bank_spec.rb # 這樣就可以跑測試了,不過我們現在啥都沒寫
6-2、先開好規格
6-2-1、存錢功能
- 可以存錢
- 不可以存0元或是小於0元的金額(越存錢越少)
6-2-2、領錢功能
- 可以領錢
- 不能領0元或是小於0元的金額
- 不能領超過本身的餘額
測試規格
> RSpec.describe House do
> describe "存錢功能" do
> it "存錢" do
> # 先把銀號帳號產生出來
> account = House.new(10)
> # 我把錢存進去
> account.deposit(4)
> # 預期得到14元
> expect(account.balance).to be 14
> end
>
> it "不可以存0元或是小於0元的金額" do
> account = House.new(10)
> account.deposit(-5)
> expect(account.balance).to be 10
> end
>
> end
>
>
> describe "領錢功能" do
> it "領錢" do
> account = House.new(10)
> money = account.withdraw(4)
> expect(money).to be 4
> expect(account.balance).to be 6
> end
>
> it "不能領0元或是小於0元的金額" do
> account = House.new(10)
> money = account.withdraw(-2)
> expect(money).to be 0
> expect(account.balance).to be 10
> end
>
> it "不能領超過本身的餘額" do
> account = House.new(10)
> money = account.withdraw(12)
> expect(money).to be 0
> expect(account.balance).to be 10
> end
> end
> end
正式寫
> class House
> def initialize(money)
> @money = money
> end
>
> def deposit(money)
> @money = @money + money if money > 0
> end
>
> def withdraw(money)
>
> return 0 if money <= 0 || balance < money
> @money = @money - money
> money
>
> end
>
> def balance
> @money
> end
> end