The Challenge

Problem Statement

Make a cli application in which user can deposit, withdraw and send money. The balance should be zero by default. For someone to send money, withdraw or deposit money he or must have correct PIN which will 1234 by default, for deposition agent number is not required. when withdraws, sends or deposits money the balance is increased or decreased by the amount withdrawn, sent or deposited in this deposition is the only action leading to balance increase.

Balance

Shoud be able to return balance when you ask for it.

Depositing

  • On depositing balance should increase by the amount deposited
  • You should return new balance and time of withdrawal

withdrawal

  • For a withdrawal to be successful
    • PIN and agent number must exist you should only check validity PIN if agent number exist if the agent number doesn't exist error message should be like so: "Agent Number does not exist, kindly verify to continue" and if the pin is invalid, "Incorrect pin, please try again"
    • equal or greater the amount being withdrawn or an error message "You have insufficient balance!"
    • On successful withdrawal, the balance should reduce by the amount withdrawn.
    • You should return new balance, time of withdrawal and random transaction code.

Sending Money

  • For sending to be successful all constraints applicable in withdrawal must be fulfilled, but you must also indicate receiving number

We are tdd guys and lets initialize our test framework(rspec).

1
2
3
4
5
6
 $cd mpesa_challange
 $rspec --init
 create   .rspec
 create   spec/spec_helper.rb
 $cd spec 
 $touch account_spec.rb 

Lets write a test to check our balance account_spec.rb 1, Edit account_spec.rb file like so.

## check balance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 require './lib/account'
    describe Account do
        it 'confirmed initial balance is 0' do
            account1 = Account.new
            expect(account1.balance).to eq 0
        end
        it 'returns my current balance' do
            balance = subject.balance
            expect(subject.check_balance).to eq "Your current balance is #{balance}"
        end
    end

Lets write the actual code account.rb 1, edit account.rb file like so.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Account 
    attr_accessor :balance  
    def initialize
        @balance = 0 
    end

    def check_balance 
        "Your current balance is #{@balance}"
    end
end

Deposit

account_spec.rb 2 Lets edit our specs to test that we will get a message with amount deposited, current amount and time it was deposited and that balance with be increased

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 require './lib/account'
    describe Account do
       #...
       #...
        it 'returns  increase balance on deposit' do
            subject.deposit(500)
            expect(subject.balance).to eq 500
        end
        it 'returns message on deposit' do      
            expect(subject.deposit(500)).to eq "Confirmed you deposited 500, your new balance 500
            at #{subject.current_time()}"
        end
    end

let make it pass like so: account.rb 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Account 
    #....
    #...
    def deposit (amount)
        @balance += amount       
        "Confirmed you deposited #{amount}, your new balance #{balance}
        at #{current_time()}"
    end 
    def current_time 
        @deposit_time  = Time.now.strftime('%d %b %Y %H:%M:%S')

    end
    #...
    #..

Withdraw

We need to be able to withdraw and get message that our withdraw was succesful.For withdraw to be successful we need valid pin, balance eeual or greater than current amount and valid agent number. But to make it simpler first let just give user ability to withdraw without checking pin, amount present or agent number. Lets write the test like so:

account_spec.rb 3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    # ...
    #...
    it 'returns reduces balance on withdrawal' do
        subject.deposit(1000)
        subject.withdraw(22222, 200, 1234 )        
        expect(subject.check_balance).to eq "Your current balance is #{800}"

    end
    it 'returns message on withdrawal' do
        subject.deposit(1000)
        expect(subject.withdraw(22222, 200, 1234 )).to eq "Confirmed you withdrew 200, your new balance 800
        at #{subject.current_time()}" 

    end
    #...
    #..

The code would be like so: account.rb 3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    #...
    #..

    def withdraw(agent_number, amount, pin)
        @balance -=amount
        "Confirmed you withdrew #{amount}, your new balance #{balance}
        at #{current_time()}"
    end
    #...
    #..

It passes now lets focus on valid pin and agent_number, and current balance amount

Lets add the test for this but let us start by refactoring our happy path test to meet this expectation account_spec.rb 4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
   it 'returns reduces balance on withdrawal' do
        agent_number = Account.agent_numbers.first        
        subject.deposit(1000)
        subject.withdraw(agent_number, 200, 1234 )        
        expect(subject.check_balance).to eq "Your current balance is 800"

    end
    it 'returns message on withdrawal' do
        agent_number = Account.agent_numbers.first
        pin = subject.pin
        subject.deposit(1000)     
        expect(subject.withdraw(agent_number, 200, pin)).to eq "Confirmed you withdrew 200, your new balance 800 at #{subject.current_time()}"
    end

Note we change those to test use valid agent_number and pin Ok now lets us write sad path for test account_spec.rb 5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    it  'returns error if agent number is invalid' do
        subject.deposit(1000) 
        expect(subject.withdraw(000, 200, 1234 )).to eq "invalid agent number try again"
    end
    it  'returns error if pin is invalid' do
        agent_number = Account.agent_numbers.first
        subject.deposit(1000) 
        expect(subject.withdraw(agent_number, 200, 12)).to eq "invalid pin number try again"
    end
    it 'returns error if current balance less than withdrawn amount' do
        agent_number = Account.agent_numbers.first
        pin = subject.pin
        balance = subject.balance
        expect(subject.withdraw(agent_number, 200, pin)).to eq "Insufficient balance,you cannot withdraw 200,your current balance is #{balance}"
    end

we add a class variable @@AGENT_NUMBERS which contains list of valid agent number and we can use class methods agent_numbers to access it . We also added attribute accessors pin this will give ability to reset the pin later and by default pin is 1234. account.rb 4

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#...
#...
    attr_accessor :balance, :pin
    @@AGENT_NUMBERS = [111111, 222222, 333333, 44444] 
    def initialize
        @balance = 0 
        @pin = 1234
    end
    def self.agent_numbers
        @@AGENT_NUMBERS 
    end
 #...
 #..

We check error messages incase agent_number provide or pin provided is invalid, check if amount being withdrawn is greater that current balance and advice user appropriately.If everything is ok transcation occurs and amount withdrawn is deducted for current balance and given new balance and time of transaction. account.rb 5

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    def withdraw(agent_number, amount, pin)
        return 'invalid agent number try again' unless @@AGENT_NUMBERS.include?(agent_number)
        return 'invalid pin number try again' unless @pin == pin
        if amount > @balance
            "Insufficient balance,you cannot withdraw #{amount},your current balance is #{@balance}"
        else
            @balance -=amount

            "Confirmed you withdrew #{amount}, your new balance #{balance} at #{current_time()}"
        end
    end

Run you test

sending money

lets deal sending money now, for money to be sent pin and phone must be correct, correct phone number is phone which is an interger and of 10 digits.Amount being withdrawn should also be equal or greater than current balance.

test should be like so: account_spec.rb 6

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    it 'reduces balance when money is sent' do
        subject.deposit(1000) 
        pin = subject.pin 
        subject.send(1234567890, 200, pin )
        expect(subject.balance).to eq 800

    end
    it 'send transaction details when money is sent' do
        subject.deposit(1000) 
        pin = subject.pin 
        expect(subject.send(1234567890, 200, pin )).to eq "Success you sent 200 to 1234567890 you current balance is 800 at #{subject.current_time}"
    end
    it 'return error if pin number is wrong when sending money' do
        expect(subject.send(1234567890, 200, 0000)).to eq 'invalid pin number try again' 
    end
    it "return errors if receiver phone number is invalid" do
        pin = subject.pin 
        expect(subject.send(12, 200, pin)).to eq 'receiver phone number is invalid'
        expect(subject.send("1234567890 ", 200, pin)).to eq 'receiver phone number is invalid'
    end

    it 'return error if balance is less than amount withdrawn' do
        pin = subject.pin     
        expect(subject.send(1234567890, 200, pin)).to eq "Insufficient balance,you cannot send 200,your current balance is 0"
    end

we do almost what we did with withdrawal the only difference is we using phone_number and not agent_number like so:

account.rb 6

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    #...
    #..

    def send(receiver, amount, pin)
        return 'invalid pin number try again' unless @pin == pin        
        return 'receiver phone number is invalid' unless receiver.to_s.length == 10 and receiver.is_a?(Integer)
        if amount > @balance
            "Insufficient balance,you cannot send #{amount},your current balance is #{@balance}"
        else
        @balance -=amount
        "Success you sent #{amount} to #{receiver} you current balance is #{@balance} at #{current_time}"
        end
    end
    #..
    #..

changing pin

Lastly let us give users ability to change their pin, for pin to be changed current pin and new pin must be given current pin should match the existing been and new pin must integer of 4 digits.The test will be like so.

account_spec.rb 7

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    #..
    #..
    it 'change the value of pin to be new pin' do
        pin = subject.pin 
        expect(subject.change_pin(pin, 8888)).to eq 'Pin successfully changed your new pin is 8888'
    end
    it 'returns error if new pin give is of bad format'  do
        pin = subject.pin 
        expect(subject.change_pin(pin, 34)).to eq "pin must be numbers only and of 4 digits" 
        expect(subject.change_pin(pin, '')).to eq "pin must be numbers only and of 4 digits" 
        expect(subject.change_pin(pin, 340000)).to eq "pin must be numbers only and of 4 digits" 
    end

    it 'returns error if pin given do not match current pin ' do
        expect(subject.change_pin(2222, 3433)).to eq  'Invalid pin kindly enter current pin number followed by new pin'
    end

account.rb 7

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    #..
    #..
    def change_pin(pin, new_pin)
        return "pin must be numbers only and of 4 digits" unless new_pin.to_s.length == 4 and new_pin.is_a?(Integer)
        if pin == @pin 
            @pin = new_pin
            "Pin successfully changed your new pin is #{@pin}"
        else
            'Invalid pin kindly enter current pin number followed by new pin'
        end
    end
    #..
    #..

Take a look at the spec file honestly it is smelling so is the implementation it is not dry let make so.Something needs to be done refactor

This is here the refactored and dry account spec file compared where there is refactored comment and the previous code

Refactor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
require './lib/account'
describe Account do
    let(:deposit) {subject.deposit(1000) } #resuable deposit
    let(:agent_number){Account.agent_numbers.first} #resuable agent_number
    let(:pin){pin = subject.pin} #resuable pin

    it 'confirms initial balance is 0' do
      account1 = Account.new
      expect(account1.balance).to eq 0
    end
    it 'returns  current balance message' do
    #refactored
     deposit
     balance = subject.balance                                      #refactored
      expect(subject.check_balance).to eq "Your current balance is 1000"
    end
    it 'returns  increase balance on deposit' do
        #refactored
        deposit                         #refactored
        expect(subject.balance).to eq 1000
    end
    it 'returns message on deposit' do      
        expect(subject.deposit(500)).to eq "Confirmed you deposited 500, your new balance 500
        at #{subject.current_time()}"
    end

    it 'returns reduces balance on withdrawal' do           
       #refactored
        deposit
        subject.withdraw(agent_number, 200, 1234 )        
        expect(subject.check_balance).to eq "Your current balance is #{800}"

    end
    it 'returns message on withdrawal' do 
        #refactored
        deposit    
        expect(subject.withdraw(agent_number, 200, pin)).to eq "Confirmed you withdrew 200, your new balance 800 at #{subject.current_time()}"
    end
    it  'returns error if agent number is invalid' do
        #refactored
        deposit
        expect(subject.withdraw(000, 200, 1234 )).to eq "invalid agent number try again"
    end
    it  'returns error if pin is invalid' do
       #refactored
        deposit
        expect(subject.withdraw(agent_number, 200, 12)).to eq "invalid pin number try again"
    end
    it 'returns error if current balance less than withdrawn amount' do        
        expect(subject.withdraw(agent_number, 200, pin)).to eq "Insufficient balance,you cannot withdraw 200,your current balance is 0"
    end
    it 'reduces balance when money is sent' do
        #refactored
        deposit
        subject.send(1234567890, 200, pin )
        expect(subject.balance).to eq 800

    end
    it 'send transaction details when money is sent' do
        #refactored
        deposit
        expect(subject.send(1234567890, 200, pin )).to eq "Success you sent 200 to 1234567890 you current balance is 800 at #{subject.current_time}"
    end
    it 'return error if pin number is wrong when sending money' do
        expect(subject.send(1234567890, 200, 0000)).to eq 'invalid pin number try again' 
    end
    it "return errors if receiver phone number is invalid" do     
        expect(subject.send(12, 200, pin)).to eq 'receiver phone number is invalid'
        expect(subject.send("1234567890 ", 200, pin)).to eq 'receiver phone number is invalid'
    end

    it 'return error if balance is less than amount sent' do
      # refactored   
        expect(subject.send(1234567890, 200, pin)).to eq "Insufficient balance,you cannot send 200,your current balance is 0"
    end
    it 'change the value of pin to be new pin' do
        #
        expect(subject.change_pin(pin, 8888)).to eq 'Pin successfully changed your new pin is 8888'
    end
    it 'returns error if new pin give is of bad format'  do
      #refactored
        expect(subject.change_pin(pin, 34)).to eq "pin must be numbers only and of 4 digits" 
        expect(subject.change_pin(pin, '')).to eq "pin must be numbers only and of 4 digits" 
        expect(subject.change_pin(pin, 340000)).to eq "pin must be numbers only and of 4 digits" 
    end

    it 'returns error if pin given do not match current pin ' do
        expect(subject.change_pin(2222, 3433)).to eq  'Invalid pin kindly enter current pin number followed by new pin'
    end

end

It still passes