code before onsite

This commit is contained in:
2019-12-17 07:25:03 +11:00
commit 8319200b4a
27 changed files with 1527 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.idea/
+8
View File
@@ -0,0 +1,8 @@
package commands
import "github.com/wahyd4/cash/domains"
// ICommand defines the banking app command
type ICommand interface {
Run(bank *domains.Bank) error
}
+18
View File
@@ -0,0 +1,18 @@
package commands
import "github.com/wahyd4/cash/domains"
// DepositCommand the command for depositing some money
type DepositCommand struct {
Account string
Money float64
}
// Run deposits some money
func (command *DepositCommand) Run(bank *domains.Bank) error {
account, err := bank.AccountFromName(command.Account)
if err != nil {
return err
}
return bank.DepositFor(account, command.Money)
}
+64
View File
@@ -0,0 +1,64 @@
package commands
import (
"github.com/wahyd4/cash/domains"
"testing"
)
func TestDepositCommand_Run(t *testing.T) {
bank := domains.InitBank()
bank.OpenAccount("Tom")
type fields struct {
Account string
Money float64
}
type args struct {
bank *domains.Bank
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
"can deposit some money",
fields{
Account: "Tom",
Money: 20.10,
},
args{bank: bank},
false,
},
{
"fail to deposit some money due to money is negative",
fields{
Account: "Tom",
Money: -20.10,
},
args{bank: bank},
true,
},
{
"fail to deposit some money due to the user is not exist",
fields{
Account: "NotExist",
Money: -20.10,
},
args{bank: bank},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
command := &DepositCommand{
Account: tt.fields.Account,
Money: tt.fields.Money,
}
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+16
View File
@@ -0,0 +1,16 @@
package commands
import (
"github.com/wahyd4/cash/domains"
)
// OpenAccountCommand the command for creating a bank account
type OpenAccountCommand struct {
Username string
}
// Run creates a bank account
func (command *OpenAccountCommand) Run(bank *domains.Bank) error {
bank.OpenAccount(command.Username)
return nil
}
+19
View File
@@ -0,0 +1,19 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/wahyd4/cash/domains"
)
func TestOpenAccountCommand_Run_ShouldCreateABankAccount(t *testing.T) {
command := OpenAccountCommand{
Username: "Mindy",
}
bank := domains.InitBank()
assert.Equal(t, len(bank.TotalAccounts()), 0)
command.Run(bank)
assert.Equal(t, len(bank.TotalAccounts()), 1)
}
+17
View File
@@ -0,0 +1,17 @@
package commands
import (
"github.com/sirupsen/logrus"
"github.com/wahyd4/cash/domains"
)
// TotalBalanceCommand for getting the balance
type TotalBalanceCommand struct {
}
// Run gets the balance for a specific account when account is not nil, otherwise for the bank
func (command *TotalBalanceCommand) Run(bank *domains.Bank) error {
logrus.Infof("the total balance is %.2f", bank.TotalBalance())
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package commands
import (
"github.com/wahyd4/cash/domains"
"testing"
)
func TestTotalBalanceCommand_Run(t *testing.T) {
bank := domains.InitBank()
bank.OpenAccount("Tom")
bank.OpenAccount("Lily")
account, _ := bank.AccountFromName("Tom")
account2, _ := bank.AccountFromName("Lily")
bank.DepositFor(account, 500)
bank.DepositFor(account2, 29.10)
type args struct {
bank *domains.Bank
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"can print balance for the whole bank",
args{bank: bank},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
command := &TotalBalanceCommand{}
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+23
View File
@@ -0,0 +1,23 @@
package commands
import (
"github.com/sirupsen/logrus"
"github.com/wahyd4/cash/domains"
)
// UserBalanceCommand for getting the balance
type UserBalanceCommand struct {
Account string
}
// Run gets balance for a specific account when account is not nil, otherwise for the bank
func (command *UserBalanceCommand) Run(bank *domains.Bank) error {
account, err := bank.AccountFromName(command.Account)
if err != nil {
return err
}
logrus.Infof("the balance of %s is %.2f", account.Name, bank.BalanceFor(account))
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package commands
import (
"github.com/wahyd4/cash/domains"
"testing"
)
func TestBalanceCommand_Run(t *testing.T) {
bank := domains.InitBank()
bank.OpenAccount("Tom")
bank.OpenAccount("Lily")
account, _ := bank.AccountFromName("Tom")
account2, _ := bank.AccountFromName("Lily")
bank.DepositFor(account, 500)
bank.DepositFor(account2, 29.10)
type fields struct {
Account string
}
type args struct {
bank *domains.Bank
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
"can print balance for user",
fields{Account: "Tom"},
args{bank: bank},
false,
},
{
"can print balance for user",
fields{Account: "Lily"},
args{bank: bank},
false,
},
{
"cannot print the user du to the user is not exist",
fields{Account: "NotExist"},
args{bank: bank},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
command := &UserBalanceCommand{
Account: tt.fields.Account,
}
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+18
View File
@@ -0,0 +1,18 @@
package commands
import "github.com/wahyd4/cash/domains"
// WithdrawCommand the command for withdrawing some money
type WithdrawCommand struct {
Account string
Money float64
}
// Run withdraws money from the bank, returns error if fails
func (command *WithdrawCommand) Run(bank *domains.Bank) error {
account, err := bank.AccountFromName(command.Account)
if err != nil {
return err
}
return bank.WithdrawFor(account, command.Money)
}
+66
View File
@@ -0,0 +1,66 @@
package commands
import (
"github.com/wahyd4/cash/domains"
"testing"
)
func TestWithdrawCommand_Run(t *testing.T) {
bank := domains.InitBank()
bank.OpenAccount("Tom")
account, _ := bank.AccountFromName("Tom")
bank.DepositFor(account, 30)
type fields struct {
Account string
Money float64
}
type args struct {
bank *domains.Bank
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
"can withdraw some money",
fields{
Account: "Tom",
Money: 20.10,
},
args{bank: bank},
false,
},
{
"cannot withdraw money due to does not have enough money",
fields{
Account: "Tom",
Money: 50.0, //we only have 9.9 left
},
args{bank: bank},
true,
},
{
"cannot withdraw due to the user can't found",
fields{
Account: "NotExist",
Money: 0.00,
},
args{bank: bank},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
command := &WithdrawCommand{
Account: tt.fields.Account,
Money: tt.fields.Money,
}
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+9
View File
@@ -0,0 +1,9 @@
version: "3"
services:
cash:
image: golang:1.13
working_dir: /cash
volumes:
- .:/cash
command: ["go", "run", "main.go", "input.txt"]
+32
View File
@@ -0,0 +1,32 @@
package domains
import "fmt"
const (
dollarToCents = 100
)
// BankAccount represents for a bank customer
type BankAccount struct {
Name string
balance int // in cent
}
// Balance get balance and return with ($)dollar instead of the unit which is cent
func (account *BankAccount) Balance() float64 {
return float64(account.balance) / float64(dollarToCents)
}
// Deposit deposits money for the account
func (account *BankAccount) Deposit(moneyInCents int) {
account.balance += moneyInCents
}
// Withdraw withdraw money for the account
func (account *BankAccount) Withdraw(moneyInCents int) error {
if account.balance < moneyInCents {
return fmt.Errorf("no enough money can be withdrawn")
}
account.balance -= moneyInCents
return nil
}
+165
View File
@@ -0,0 +1,165 @@
package domains
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestBankAccount_Balance(t *testing.T) {
type fields struct {
Name string
balance int
}
tests := []struct {
name string
fields fields
want float64
}{
{
"balance is 0",
fields{
Name: "Tom",
balance: 0,
},
0.00,
},
{
"balance is not 0 and no extra cents",
fields{
Name: "Lily",
balance: 178800,
},
1788.00,
},
{
"balance is not 0 and with some extra cents",
fields{
Name: "Dave",
balance: 8912,
},
89.12,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account := &BankAccount{
Name: tt.fields.Name,
balance: tt.fields.balance,
}
if got := account.Balance(); got != tt.want {
t.Errorf("Balance() = %v, want %v", got, tt.want)
}
})
}
}
func TestBankAccount_Deposit(t *testing.T) {
type fields struct {
Name string
balance int
}
type args struct {
moneyInCents int
}
tests := []struct {
name string
fields fields
args args
}{
{
name: "deposits some money when balance is 0",
fields: fields{
Name: "Tom",
balance: 0,
},
args: args{moneyInCents: 3500},
},
{
name: "deposits some money when balance is not 0",
fields: fields{
Name: "Tom",
balance: 2800,
},
args: args{moneyInCents: 3500},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account := &BankAccount{
Name: tt.fields.Name,
balance: tt.fields.balance,
}
account.Deposit(tt.args.moneyInCents)
assert.Equal(t, account.balance, tt.fields.balance+tt.args.moneyInCents)
})
}
}
func TestBankAccount_Withdraw1(t *testing.T) {
type fields struct {
Name string
balance int
}
type args struct {
moneyInCents int
}
tests := []struct {
name string
fields fields
args args
expectedBalance int
wantErr bool
}{
{
"can withdraw some money when user has enough balance",
fields{
Name: "Tom",
balance: 10000,
},
args{moneyInCents: 3500},
6500,
false,
},
{
"can withdraw some money when user withdraw all the money he has",
fields{
Name: "Tom",
balance: 8765,
},
args{moneyInCents: 8765},
0,
false,
},
{
"can't withdraw the money more than the user has",
fields{
Name: "Tom",
balance: 8765,
},
args{moneyInCents: 9888},
8765,
true,
},
{
"can't withdraw money more than the user has",
fields{
Name: "Tom",
balance: 8765,
},
args{moneyInCents: 10000},
8765,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
account := &BankAccount{
Name: tt.fields.Name,
balance: tt.fields.balance,
}
if err := account.Withdraw(tt.args.moneyInCents); (err != nil) != tt.wantErr {
t.Errorf("Withdraw() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+95
View File
@@ -0,0 +1,95 @@
package domains
import (
"fmt"
"github.com/sirupsen/logrus"
)
var (
ErrActionMoneyAmountInvalid = fmt.Errorf("money can't be negative or 0")
)
const roundFloat = 0.5
// Bank represents for the bank
// can be improved by using sync.Mutex
type Bank struct {
accounts []*BankAccount
totalBalance int
}
// InitBank inits the bank
func InitBank() *Bank {
return &Bank{}
}
// OpenAccount opens a account for customer
func (bank *Bank) OpenAccount(customerName string) {
bank.accounts = append(bank.accounts, &BankAccount{Name: customerName})
logrus.Infof("opened bank account for: %s", customerName)
}
// DepositFor deposit for a user
func (bank *Bank) DepositFor(account *BankAccount, money float64) error {
if money <= 0 {
return ErrActionMoneyAmountInvalid
}
moneyInCents := toCents(money)
account.Deposit(moneyInCents)
bank.totalBalance += moneyInCents
logrus.Infof("deposited $%.2f for %s", money, account.Name)
return nil
}
// WithdrawFor withdraw money for a user
func (bank *Bank) WithdrawFor(account *BankAccount, money float64) error {
if money <= 0 {
return ErrActionMoneyAmountInvalid
}
moneyInCents := toCents(money)
if err := account.Withdraw(moneyInCents); err != nil {
return fmt.Errorf("failed to withdraw money %.2f due to %v", money, err)
}
bank.totalBalance -= moneyInCents
logrus.Infof("withdrawn $%.2f for %s", money, account.Name)
return nil
}
// BalanceFor gets balance for a user
func (bank *Bank) BalanceFor(account *BankAccount) float64 {
return float64(account.balance) / float64(dollarToCents)
}
// TotalBalance gets total balance for the bank
func (bank *Bank) TotalBalance() float64 {
return float64(bank.totalBalance) / float64(dollarToCents)
}
// TotalAccounts returns all the bank accounts for the bank
func (bank *Bank) TotalAccounts() []*BankAccount {
return bank.accounts
}
// AccountFromName finds account by name, returns error if not found.
func (bank *Bank) AccountFromName(accountName string) (*BankAccount, error) {
for _, account := range bank.accounts {
if account.Name == accountName {
return account, nil
}
}
return nil, fmt.Errorf("no account with name %s", accountName)
}
// toCents convert dollar to cents and make sure we get accurate result
// e.g. 1.23 to $1.23, 1.345 to $1.35
func toCents(moneyInDollar float64) int {
return int(moneyInDollar*dollarToCents + roundFloat)
}
+408
View File
@@ -0,0 +1,408 @@
package domains
import (
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)
func TestInitBank(t *testing.T) {
tests := []struct {
name string
want *Bank
}{
{
"init bank with empty account list and 0 balance",
&Bank{
accounts: []*BankAccount{},
totalBalance: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := InitBank(); (len(got.accounts) != len(tt.want.accounts)) || (got.totalBalance != tt.want.totalBalance) {
t.Errorf("InitBank() = %v, want %v", got, tt.want)
}
})
}
}
func TestBank_OpenAccount(t *testing.T) {
bank := InitBank()
t.Run("should open a account for customer", func(t *testing.T) {
bank.OpenAccount("Nick")
assert.Equal(t, len(bank.accounts), 1)
assert.Equal(t, bank.totalBalance, 0)
})
}
func TestBank_DepositFor(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
type args struct {
account *BankAccount
money float64
}
tom := &BankAccount{balance: 0, Name: "Tom"}
tests := []struct {
name string
fields fields
args args
expectedBalance int
wantErr bool
}{
{
"can deposit some money",
fields{
accounts: []*BankAccount{tom},
totalBalance: 0,
},
args{
account: tom,
money: 12.34,
},
1234,
false,
},
{
"can deposit some money",
fields{
accounts: []*BankAccount{tom},
totalBalance: 6700,
},
args{
account: tom,
money: 210,
},
27700,
false,
},
{
"cannot deposit when money is negative",
fields{
accounts: []*BankAccount{tom},
totalBalance: 6700,
},
args{
account: tom,
money: -100,
},
6700,
true,
},
{
"cannot deposit when money is 0",
fields{
accounts: []*BankAccount{tom},
totalBalance: 6700,
},
args{
account: tom,
money: 0,
},
6700,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
err := bank.DepositFor(tt.args.account, tt.args.money)
if (err != nil) != tt.wantErr {
t.Errorf("DepositFor() error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
assert.Equal(t, tt.expectedBalance, bank.totalBalance)
}
})
}
}
func TestBank_WithdrawFor(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
type args struct {
account *BankAccount
money float64
}
tests := []struct {
name string
fields fields
args args
expectedBalance int
wantErr bool
}{
{
"can withdraw some money",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
totalBalance: 10000,
},
args{
account: &BankAccount{Name: "Tom", balance: 8000},
money: 12.34,
},
8766,
false,
},
{
"can withdraw all money the user has",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
totalBalance: 5678,
},
args{
account: &BankAccount{Name: "Tom", balance: 8000},
money: 56.78,
},
0,
false,
},
{
"cannot withdraw 0",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
totalBalance: 10000,
},
args{
account: &BankAccount{Name: "Tom", balance: 8000},
money: 0,
},
10000,
true,
},
{
"cannot withdraw negative",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
totalBalance: 10000,
},
args{
account: &BankAccount{Name: "Tom", balance: 8000},
money: -23,
},
10000,
true,
},
{
"cannot withdraw the money more than the user has",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 800}},
totalBalance: 800,
},
args{
account: &BankAccount{Name: "Tom", balance: 800},
money: 20.88,
},
800,
true,
},
{
"cannot withdraw the money more than the user has but bank has enough money",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 800}},
totalBalance: 1800,
},
args{
account: &BankAccount{Name: "Tom", balance: 800},
money: 20.88,
},
800,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
err := bank.WithdrawFor(tt.args.account, tt.args.money)
if (err != nil) != tt.wantErr {
t.Errorf("WithdrawFor() error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
assert.Equal(t, tt.expectedBalance, bank.totalBalance)
}
})
}
}
func TestBank_BalanceFor(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
type args struct {
account *BankAccount
}
tests := []struct {
name string
fields fields
args args
want float64
}{
{
"can get balance a user when user has some money",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 812}},
totalBalance: 1800,
},
args{
account: &BankAccount{Name: "Tom", balance: 812},
},
8.12,
},
{
"can get balance a user when user does not have any money",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 0}},
totalBalance: 1800,
},
args{
account: &BankAccount{Name: "Tom", balance: 0},
},
0.00,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
if got := bank.BalanceFor(tt.args.account); got != tt.want {
t.Errorf("BalanceFor() = %v, want %v", got, tt.want)
}
})
}
}
func TestBank_TotalBalance(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
tests := []struct {
name string
fields fields
want float64
}{
{
"can get balance for the bank",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 812}},
totalBalance: 1876,
},
18.76,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
if got := bank.TotalBalance(); got != tt.want {
t.Errorf("TotalBalance() = %v, want %v", got, tt.want)
}
})
}
}
func TestBank_TotalAccounts(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
tests := []struct {
name string
fields fields
want []*BankAccount
}{
{
"can get all the bank accounts",
fields{
[]*BankAccount{{Name: "Tom", balance: 812}},
1000,
},
[]*BankAccount{{Name: "Tom", balance: 812}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
if got := bank.TotalAccounts(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("TotalAccounts() = %v, want %v", got, tt.want)
}
})
}
}
func TestBank_AccountFromName(t *testing.T) {
type fields struct {
accounts []*BankAccount
totalBalance int
}
type args struct {
accountName string
}
tests := []struct {
name string
fields fields
args args
want *BankAccount
wantErr bool
}{
{
"can find account by name",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 812}, {Name: "Lily", balance: 0}},
totalBalance: 812,
},
args{accountName: "Lily"},
&BankAccount{Name: "Lily", balance: 0},
false,
},
{
"can not find account due to the account not exist",
fields{
accounts: []*BankAccount{{Name: "Tom", balance: 812}, {Name: "Lily", balance: 0}},
totalBalance: 812,
},
args{accountName: "SomeFakeName"},
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bank := &Bank{
accounts: tt.fields.accounts,
totalBalance: tt.fields.totalBalance,
}
got, err := bank.AccountFromName(tt.args.accountName)
if (err != nil) != tt.wantErr {
t.Errorf("AccountFromName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AccountFromName() got = %v, want %v", got, tt.want)
}
})
}
}
+9
View File
@@ -0,0 +1,9 @@
module github.com/wahyd4/cash
go 1.13
require (
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
)
+21
View File
@@ -0,0 +1,21 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+12
View File
@@ -0,0 +1,12 @@
OPEN_ACCOUNT Tom
DEPOSIT Tom 520.30
BALANCE Tom
TOTAL_BALANCE
WITHDRAW Tom 340.00
BALANCE Tom
OPEN_ACCOUNT Lily
DEPOSIT Lily 1980.55
BALANCE Lily
WITHDRAW Lily 550.33
BALANCE Lily
TOTAL_BALANCE
+30
View File
@@ -0,0 +1,30 @@
package main
import (
"os"
"github.com/sirupsen/logrus"
"github.com/wahyd4/cash/domains"
"github.com/wahyd4/cash/workflow"
)
func main() {
if len(os.Args) != 2 {
logrus.Fatal("Please run the cash application by: go run main.go <input.txt>")
}
filePathParam := os.Args[1]
bank := domains.InitBank()
app := workflow.APP{Bank: bank}
commands, err := app.Load(filePathParam)
if err != nil {
logrus.Panic("fail to load input" + err.Error())
}
if err := app.Run(commands); err != nil {
logrus.Error(err)
}
}
+6
View File
@@ -0,0 +1,6 @@
OPEN_ACCOUNT Tom
DEPOSIT Tom 123
BALANCE Tom
WITHDRAW Tom 250
BALANCE Tom
TOTAL_BALANCE
+107
View File
@@ -0,0 +1,107 @@
# Cash
- [Cash](#cash)
- [About the solution](#about-the-solution)
- [Stack](#stack)
- [Design](#design)
- [Assumptions &amp; Trade offs](#assumptions-amp-trade-offs)
- [What would I do more if I have more time](#what-would-i-do-more-if-i-have-more-time)
- [How to run](#how-to-run)
- [docker-compose](#docker-compose)
- [The classic Way](#the-classic-way)
- [Run the application against custom inputs](#run-the-application-against-custom-inputs)
## About the solution
### Stack
- Go
- [Logrus](https://github.com/sirupsen/logrus) for better logging
- [testify/assert](https://github.com/stretchr/testify) some test assertion helpers
### Design
In my design, there are `4` core domains,
- Application, represents the cash application which can load inputs and execute actions.
- BankAccount, contains the information of a single bank account and basic deposit/withdraw actions to the user.
- Bank, the bank entity which holds all the bank accounts and the total balance. All the `deposit/withdraw` actions go to bank and then acknowledged by bank account.
- Command, represent a single action. e.g. `deposit`, `withdraw`, `balance`. way. All commands have been implemented the `ICommand` interface with `Run(bank *Bank)` method, so they can be processed by the application in a standard
The steps below shows how the application works.
1. When we start this application it will initialise a `bank` and the `cash application` instance.
2. All the actions recorded in a input file will be parsed and transferred to `command`
3. The Cash application will take the commands as input, process them and print out some basic information.
### Assumptions & Trade offs
In order to start implementing this project, I have made a few assumptions below:
- Only one seesion for any user at any single moment, which means there will no dirty reads/writes for a user
- All the actions will be loaded through a input text file and will be processed following the sequence order, so it means the application currently can only run one command at a time.
- I use Australian Dollar($) as the currency type, use `int` to represent `cent` as the currency unit.
### What would I do more if I have more time
There are two things I want to do most in terms of building a proper cash app if I have more time.
- Make it to be a CLI and support multiple users login and run actions at the same time.
- Add some basic locks for depositing and withdrawing operations at both user and bank level.
## How to run
### docker-compose
Please make sure you have `docker` and `docker-compose` installed
Then just simply run the commands below
```bash
# One command to run application
docker-compose up
# If you want to run another failure case with withdrawing more money than the user has
docker-compose run cash go run main.go no_enough_money.txt
# Run tests
docker-compose run cash go test ./... -cover
```
Or you can try the classic way.
### The classic Way
Similar, you should have `go` installed before we start
```bash
# Install all dependencies
go mod download
# Run application
go run main.go input.txt
# Run Tests
go test ./... -cover
```
### Run the application against custom inputs
You can also build your custom input and define as many commands as you want.
There is a example below with single bank account.
```
OPEN_ACCOUNT Tom
DEPOSIT Tom 520.30
WITHDRAW Tom 340.00
BALANCE Tom
TOTAL_BALANCE
```
Then test it
```bash
docker-compose run cash go run main.go yourfile
```
+80
View File
@@ -0,0 +1,80 @@
package workflow
import (
"bufio"
"fmt"
"github.com/wahyd4/cash/commands"
"github.com/wahyd4/cash/domains"
"os"
"strconv"
"strings"
)
const commandSplitter = " "
// APP holds the bank app instance
type APP struct {
Bank *domains.Bank
}
// Run all the commands passed in
func (app *APP) Run(commands []commands.ICommand) error {
for _, command := range commands {
if err := command.Run(app.Bank); err != nil {
return err
}
}
return nil
}
// Load loads inputs and generate commands
func (app *APP) Load(filePath string) ([]commands.ICommand, error) {
appCommands := make([]commands.ICommand, 0)
file, err := os.Open(filePath)
if err != nil {
return appCommands, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
commandString := scanner.Text()
appCommands = append(appCommands, buildCommand(commandString))
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("fail to read file %s with error: %v", filePath, err)
}
return appCommands, nil
}
func buildCommand(commandString string) commands.ICommand {
commandArgs := strings.Split(commandString, commandSplitter)
switch commandArgs[0] {
case "OPEN_ACCOUNT":
return &commands.OpenAccountCommand{Username: commandArgs[1]}
case "DEPOSIT":
money, _ := strconv.ParseFloat(commandArgs[2], 10)
return &commands.DepositCommand{
Account: commandArgs[1],
Money: money,
}
case "WITHDRAW":
money, _ := strconv.ParseFloat(commandArgs[2], 10)
return &commands.WithdrawCommand{
Account: commandArgs[1],
Money: money,
}
case "BALANCE":
return &commands.UserBalanceCommand{
Account: commandArgs[1],
}
case "TOTAL_BALANCE":
return &commands.TotalBalanceCommand{}
default:
return &commands.TotalBalanceCommand{}
}
}
+185
View File
@@ -0,0 +1,185 @@
package workflow
import (
"errors"
"github.com/stretchr/testify/assert"
"github.com/wahyd4/cash/commands"
"github.com/wahyd4/cash/domains"
"reflect"
"testing"
)
// simpleCommand a dump implementation for test
type simpleCommand struct {
shouldErr bool
}
func (command *simpleCommand) Run(bank *domains.Bank) error {
if command.shouldErr {
return errors.New("some error")
}
return nil
}
func TestAPP_Run(t *testing.T) {
type fields struct {
Bank *domains.Bank
}
type args struct {
commands []commands.ICommand
}
bank := domains.InitBank()
bank.OpenAccount("Dan")
tests := []struct {
name string
fields fields
args args
wantErr bool
totalBalanceInFloat float64
}{
{
"can finish all commands successfully",
fields{Bank: bank},
args{[]commands.ICommand{&simpleCommand{false}}},
false,
0.00,
},
{
"fail to run some commands",
fields{Bank: bank},
args{[]commands.ICommand{&simpleCommand{true}}},
true,
0.00,
},
{
"run real commands",
fields{Bank: bank},
args{[]commands.ICommand{
&commands.OpenAccountCommand{Username: "Tom"},
&commands.DepositCommand{
Account: "Tom",
Money: 520.30,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.WithdrawCommand{
Account: "Tom",
Money: 340.00,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.TotalBalanceCommand{},
},},
false,
180.30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &APP{
Bank: tt.fields.Bank,
}
err := app.Run(tt.args.commands)
if (err != nil) != tt.wantErr {
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
assert.Equal(t, tt.totalBalanceInFloat, bank.TotalBalance())
}
})
}
}
func TestAPP_Load(t *testing.T) {
bank := domains.InitBank()
type fields struct {
Bank *domains.Bank
}
type args struct {
filePath string
}
tests := []struct {
name string
fields fields
args args
want []commands.ICommand
wantErr bool
}{
{
"build commands from file input",
fields{Bank: bank},
args{filePath: "test_input.txt"},
[]commands.ICommand{
&commands.OpenAccountCommand{Username: "Tom"},
&commands.DepositCommand{
Account: "Tom",
Money: 520.30,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.WithdrawCommand{
Account: "Tom",
Money: 340.00,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.TotalBalanceCommand{},
},
false,
},
{
"build commands from file input for multiple users",
fields{Bank: bank},
args{filePath: "test_input_multiple_users.txt"},
[]commands.ICommand{
&commands.OpenAccountCommand{Username: "Tom"},
&commands.DepositCommand{
Account: "Tom",
Money: 520.30,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.WithdrawCommand{
Account: "Tom",
Money: 340.00,
},
&commands.UserBalanceCommand{Account: "Tom"},
&commands.OpenAccountCommand{Username: "Lily"},
&commands.DepositCommand{
Account: "Lily",
Money: 1980.55,
},
&commands.UserBalanceCommand{Account: "Lily"},
&commands.WithdrawCommand{
Account: "Lily",
Money: 550.33,
},
&commands.UserBalanceCommand{Account: "Lily"},
&commands.TotalBalanceCommand{},
},
false,
},
{
"fail to load inputs",
fields{Bank: bank},
args{filePath: "doesnt_exist.txt"},
[]commands.ICommand{},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &APP{
Bank: tt.fields.Bank,
}
got, err := app.Load(tt.args.filePath)
if (err != nil) != tt.wantErr {
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Load() got = %v, want %v", got, tt.want)
}
})
}
}
+6
View File
@@ -0,0 +1,6 @@
OPEN_ACCOUNT Tom
DEPOSIT Tom 520.30
BALANCE Tom
WITHDRAW Tom 340.00
BALANCE Tom
TOTAL_BALANCE
+11
View File
@@ -0,0 +1,11 @@
OPEN_ACCOUNT Tom
DEPOSIT Tom 520.30
BALANCE Tom
WITHDRAW Tom 340.00
BALANCE Tom
OPEN_ACCOUNT Lily
DEPOSIT Lily 1980.55
BALANCE Lily
WITHDRAW Lily 550.33
BALANCE Lily
TOTAL_BALANCE