From cb2dfd919548f42876d30df5d4e2ed4aaee46f8f Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 16:34:26 +0800 Subject: [PATCH 1/6] feat: add migration --- db/migration/000003_add_account_type.down.sql | 7 +++++++ db/migration/000003_add_account_type.up.sql | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 db/migration/000003_add_account_type.down.sql create mode 100644 db/migration/000003_add_account_type.up.sql diff --git a/db/migration/000003_add_account_type.down.sql b/db/migration/000003_add_account_type.down.sql new file mode 100644 index 0000000..5c19f42 --- /dev/null +++ b/db/migration/000003_add_account_type.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE "accounts" DROP CONSTRAINT "uq_owner_currency_type"; + +ALTER TABLE "accounts" DROP COLUMN acc_type + +ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency"); + +DROP TYPE ACCOUNT_TYPE; \ No newline at end of file diff --git a/db/migration/000003_add_account_type.up.sql b/db/migration/000003_add_account_type.up.sql new file mode 100644 index 0000000..11b391d --- /dev/null +++ b/db/migration/000003_add_account_type.up.sql @@ -0,0 +1,7 @@ +CREATE TYPE ACCOUNT_TYPE AS ENUM ('bank', 'external', 'credit_card'); + +ALTER TABLE "accounts" DROP CONSTRAINT "owner_currency_key"; + +ALTER TABLE "accounts" ADD acc_type ACCOUNT_TYPE NOT NULL DEFAULT('bank'); + +ALTER TABLE "accounts" ADD CONSTRAINT "uq_owner_currency_type" UNIQUE ("owner", "currency", "acc_type"); \ No newline at end of file From 111c9cff9981aa9f879158fd77a21a137d9eacd1 Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 16:34:56 +0800 Subject: [PATCH 2/6] feat: update db sqlc and mock --- db/mock/store.go | 30 ++++++++++++++ db/query/account.sql | 20 ++++++--- db/sqlc/account.sql.go | 74 +++++++++++++++++++++++++++------- db/sqlc/account_test.go | 1 + db/sqlc/create_user_tx.go | 29 +++++++++++++ db/sqlc/create_user_tx_test.go | 31 ++++++++++++++ db/sqlc/db.go | 2 + db/sqlc/entry.sql.go | 2 + db/sqlc/models.go | 58 +++++++++++++++++++++++--- db/sqlc/querier.go | 3 ++ db/sqlc/store.go | 1 + db/sqlc/transfer.sql.go | 2 + db/sqlc/user.sql.go | 2 + 13 files changed, 230 insertions(+), 25 deletions(-) create mode 100644 db/sqlc/create_user_tx.go create mode 100644 db/sqlc/create_user_tx_test.go diff --git a/db/mock/store.go b/db/mock/store.go index a114a52..39a5583 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -65,6 +65,21 @@ func (mr *MockStoreMockRecorder) CreateAccount(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockStore)(nil).CreateAccount), arg0, arg1) } +// CreateAccountTx mocks base method. +func (m *MockStore) CreateAccountTx(arg0 context.Context, arg1 db.CreateAccountParams) (db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAccountTx", arg0, arg1) + ret0, _ := ret[0].(db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateAccountTx indicates an expected call of CreateAccountTx. +func (mr *MockStoreMockRecorder) CreateAccountTx(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccountTx", reflect.TypeOf((*MockStore)(nil).CreateAccountTx), arg0, arg1) +} + // CreateEntry mocks base method. func (m *MockStore) CreateEntry(arg0 context.Context, arg1 db.CreateEntryParams) (db.Entry, error) { m.ctrl.T.Helper() @@ -169,6 +184,21 @@ func (mr *MockStoreMockRecorder) GetEntry(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntry", reflect.TypeOf((*MockStore)(nil).GetEntry), arg0, arg1) } +// GetExtAccount mocks base method. +func (m *MockStore) GetExtAccount(arg0 context.Context, arg1 db.GetExtAccountParams) (db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExtAccount", arg0, arg1) + ret0, _ := ret[0].(db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetExtAccount indicates an expected call of GetExtAccount. +func (mr *MockStoreMockRecorder) GetExtAccount(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtAccount", reflect.TypeOf((*MockStore)(nil).GetExtAccount), arg0, arg1) +} + // GetTransfer mocks base method. func (m *MockStore) GetTransfer(arg0 context.Context, arg1 int64) (db.Transfer, error) { m.ctrl.T.Helper() diff --git a/db/query/account.sql b/db/query/account.sql index a589dba..adc39c1 100644 --- a/db/query/account.sql +++ b/db/query/account.sql @@ -2,23 +2,33 @@ INSERT INTO accounts ( owner, balance, - currency + currency, + acc_type ) VALUES ( - $1, $2, $3 + $1, $2, $3, $4 ) RETURNING *; -- name: GetAccount :one SELECT * FROM accounts -WHERE id = $1 LIMIT 1; +WHERE id = $1 AND acc_type = 'bank' +LIMIT 1; -- name: GetAccountForUpdate :one SELECT * FROM accounts -WHERE id = $1 LIMIT 1 +WHERE id = $1 + AND acc_type = 'bank' +LIMIT 1 FOR NO KEY UPDATE; +-- name: GetExtAccount :one +SELECT * FROM accounts +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +LIMIT 1; + -- name: ListAccounts :many SELECT * FROM accounts -WHERE owner = $1 +WHERE owner = $1 +AND acc_type = 'bank' ORDER BY id LIMIT $2 OFFSET $3; diff --git a/db/sqlc/account.sql.go b/db/sqlc/account.sql.go index 86cb410..988d8cc 100644 --- a/db/sqlc/account.sql.go +++ b/db/sqlc/account.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 // source: account.sql package db @@ -11,7 +13,7 @@ const addAccountBalance = `-- name: AddAccountBalance :one UPDATE accounts SET balance = balance+ $1 WHERE id = $2 -RETURNING id, owner, balance, currency, created_at +RETURNING id, owner, balance, currency, created_at, acc_type ` type AddAccountBalanceParams struct { @@ -28,6 +30,7 @@ func (q *Queries) AddAccountBalance(ctx context.Context, arg AddAccountBalancePa &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, ) return i, err } @@ -36,20 +39,27 @@ const createAccount = `-- name: CreateAccount :one INSERT INTO accounts ( owner, balance, - currency + currency, + acc_type ) VALUES ( - $1, $2, $3 -) RETURNING id, owner, balance, currency, created_at + $1, $2, $3, $4 +) RETURNING id, owner, balance, currency, created_at, acc_type ` type CreateAccountParams struct { - Owner string `json:"owner"` - Balance int64 `json:"balance"` - Currency string `json:"currency"` + Owner string `json:"owner"` + Balance int64 `json:"balance"` + Currency string `json:"currency"` + AccType AccountType `json:"acc_type"` } func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { - row := q.db.QueryRowContext(ctx, createAccount, arg.Owner, arg.Balance, arg.Currency) + row := q.db.QueryRowContext(ctx, createAccount, + arg.Owner, + arg.Balance, + arg.Currency, + arg.AccType, + ) var i Account err := row.Scan( &i.ID, @@ -57,6 +67,7 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, ) return i, err } @@ -72,8 +83,9 @@ func (q *Queries) DeleteAccount(ctx context.Context, id int64) error { } const getAccount = `-- name: GetAccount :one -SELECT id, owner, balance, currency, created_at FROM accounts -WHERE id = $1 LIMIT 1 +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE id = $1 AND acc_type = 'bank' +LIMIT 1 ` func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { @@ -85,13 +97,16 @@ func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, ) return i, err } const getAccountForUpdate = `-- name: GetAccountForUpdate :one -SELECT id, owner, balance, currency, created_at FROM accounts -WHERE id = $1 LIMIT 1 +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE id = $1 + AND acc_type = 'bank' +LIMIT 1 FOR NO KEY UPDATE ` @@ -104,13 +119,40 @@ func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, e &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, + ) + return i, err +} + +const getExtAccount = `-- name: GetExtAccount :one +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +LIMIT 1 +` + +type GetExtAccountParams struct { + Owner string `json:"owner"` + Currency string `json:"currency"` +} + +func (q *Queries) GetExtAccount(ctx context.Context, arg GetExtAccountParams) (Account, error) { + row := q.db.QueryRowContext(ctx, getExtAccount, arg.Owner, arg.Currency) + var i Account + err := row.Scan( + &i.ID, + &i.Owner, + &i.Balance, + &i.Currency, + &i.CreatedAt, + &i.AccType, ) return i, err } const listAccounts = `-- name: ListAccounts :many -SELECT id, owner, balance, currency, created_at FROM accounts -WHERE owner = $1 +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE owner = $1 +AND acc_type = 'bank' ORDER BY id LIMIT $2 OFFSET $3 @@ -137,6 +179,7 @@ func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]A &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, ); err != nil { return nil, err } @@ -155,7 +198,7 @@ const updateAccount = `-- name: UpdateAccount :one UPDATE accounts SET balance = $2 WHERE id = $1 -RETURNING id, owner, balance, currency, created_at +RETURNING id, owner, balance, currency, created_at, acc_type ` type UpdateAccountParams struct { @@ -172,6 +215,7 @@ func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (A &i.Balance, &i.Currency, &i.CreatedAt, + &i.AccType, ) return i, err } diff --git a/db/sqlc/account_test.go b/db/sqlc/account_test.go index 324d4ba..5682f61 100644 --- a/db/sqlc/account_test.go +++ b/db/sqlc/account_test.go @@ -15,6 +15,7 @@ func createRandomAccount(t *testing.T) Account { Owner: user.Username, Balance: util.RandomMoney(), Currency: util.RandomCurrency(), + AccType: AccountTypeBank, } account, err := testQueries.CreateAccount(context.Background(), args) diff --git a/db/sqlc/create_user_tx.go b/db/sqlc/create_user_tx.go new file mode 100644 index 0000000..3202db7 --- /dev/null +++ b/db/sqlc/create_user_tx.go @@ -0,0 +1,29 @@ +package db + +import "context" + +func (store *SQLStore) CreateAccountTx(ctx context.Context, param CreateAccountParams) (Account, error) { + // 1. create account + // 2. create external account (double entry) + var acc Account + err := store.execTx(ctx, func(q *Queries) error { + var err_ error + acc, err_ = q.CreateAccount(ctx, param) + if err_ != nil { + return err_ + } + param_ext := CreateAccountParams{ + Owner: param.Owner, + Currency: param.Currency, + Balance: 0, + AccType: AccountTypeExternal, + } + _, err_ = q.CreateAccount(ctx, param_ext) + if err_ != nil { + return err_ + } + return nil + }) + + return acc, err +} diff --git a/db/sqlc/create_user_tx_test.go b/db/sqlc/create_user_tx_test.go new file mode 100644 index 0000000..7d791ed --- /dev/null +++ b/db/sqlc/create_user_tx_test.go @@ -0,0 +1,31 @@ +package db + +import ( + "context" + "testing" + + "github.com/hhow09/simple_bank/util" + "github.com/stretchr/testify/require" +) + +func TestCreateAccountTx(t *testing.T) { + store := NewStore(testDB) + user := createRandomUser(t) + args := CreateAccountParams{ + Owner: user.Username, + Balance: util.RandomMoney(), + Currency: util.RandomCurrency(), + AccType: AccountTypeBank, + } + acc, err := store.CreateAccountTx(context.Background(), args) + require.NoError(t, err) + require.NotEmpty(t, acc) + + accExt, err := store.GetExtAccount(context.Background(), GetExtAccountParams{Owner: user.Username, Currency: acc.Currency}) + require.NoError(t, err) + require.NotEmpty(t, accExt) + require.Equal(t, accExt.Owner, acc.Owner) + require.Equal(t, accExt.Balance, int64(0)) + require.Equal(t, accExt.Currency, acc.Currency) + require.Equal(t, accExt.AccType, AccountTypeExternal) +} diff --git a/db/sqlc/db.go b/db/sqlc/db.go index c3c034a..35e5f4a 100644 --- a/db/sqlc/db.go +++ b/db/sqlc/db.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 package db diff --git a/db/sqlc/entry.sql.go b/db/sqlc/entry.sql.go index d31872f..98833e8 100644 --- a/db/sqlc/entry.sql.go +++ b/db/sqlc/entry.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 // source: entry.sql package db diff --git a/db/sqlc/models.go b/db/sqlc/models.go index b424990..56a9fcf 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -1,17 +1,65 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 package db import ( + "database/sql/driver" + "fmt" "time" ) +type AccountType string + +const ( + AccountTypeBank AccountType = "bank" + AccountTypeExternal AccountType = "external" + AccountTypeCreditCard AccountType = "credit_card" +) + +func (e *AccountType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = AccountType(s) + case string: + *e = AccountType(s) + default: + return fmt.Errorf("unsupported scan type for AccountType: %T", src) + } + return nil +} + +type NullAccountType struct { + AccountType AccountType + Valid bool // Valid is true if AccountType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullAccountType) Scan(value interface{}) error { + if value == nil { + ns.AccountType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.AccountType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullAccountType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.AccountType), nil +} + type Account struct { - ID int64 `json:"id"` - Owner string `json:"owner"` - Balance int64 `json:"balance"` - Currency string `json:"currency"` - CreatedAt time.Time `json:"created_at"` + ID int64 `json:"id"` + Owner string `json:"owner"` + Balance int64 `json:"balance"` + Currency string `json:"currency"` + CreatedAt time.Time `json:"created_at"` + AccType AccountType `json:"acc_type"` } type Entry struct { diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 82434e7..67834b5 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 package db @@ -16,6 +18,7 @@ type Querier interface { GetAccount(ctx context.Context, id int64) (Account, error) GetAccountForUpdate(ctx context.Context, id int64) (Account, error) GetEntry(ctx context.Context, id int64) (Entry, error) + GetExtAccount(ctx context.Context, arg GetExtAccountParams) (Account, error) GetTransfer(ctx context.Context, id int64) (Transfer, error) GetUser(ctx context.Context, username string) (User, error) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) diff --git a/db/sqlc/store.go b/db/sqlc/store.go index 47c711b..a9c8a21 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -12,6 +12,7 @@ import ( type Store interface { Querier TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) + CreateAccountTx(ctx context.Context, arg CreateAccountParams) (Account, error) } // SQLStore provides all funcs to execute queries and transactions diff --git a/db/sqlc/transfer.sql.go b/db/sqlc/transfer.sql.go index 619c183..4c3caf3 100644 --- a/db/sqlc/transfer.sql.go +++ b/db/sqlc/transfer.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 // source: transfer.sql package db diff --git a/db/sqlc/user.sql.go b/db/sqlc/user.sql.go index 564f127..7ddde40 100644 --- a/db/sqlc/user.sql.go +++ b/db/sqlc/user.sql.go @@ -1,4 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 // source: user.sql package db From bc1b625f0fce005ba5698826d3affb7ab4f4bbc8 Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 16:35:06 +0800 Subject: [PATCH 3/6] feat: update code and test --- Makefile | 2 +- api/account_test.go | 7 ++++--- api/controllers/account_controller.go | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index bc67caf..8108ac8 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ sqlc: sqlc generate test: - go test -v -cover ./... + go test -cover ./... server: go run main.go diff --git a/api/account_test.go b/api/account_test.go index 4665b92..14c2306 100644 --- a/api/account_test.go +++ b/api/account_test.go @@ -173,8 +173,9 @@ func TestCreateAccountAPI(t *testing.T) { Owner: account.Owner, Currency: account.Currency, Balance: 0, + AccType: db.AccountTypeBank, } - store.EXPECT().CreateAccount(gomock.Any(), arg).Times(1).Return(account, nil) + store.EXPECT().CreateAccountTx(gomock.Any(), arg).Times(1).Return(account, nil) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusOK, recorder.Code) @@ -191,7 +192,7 @@ func TestCreateAccountAPI(t *testing.T) { addAuth(t, request, tokenMaker, constants.AuthTypeBearer, user.Username, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { - store.EXPECT().CreateAccount(gomock.Any(), gomock.Any()).Times(1).Return(db.Account{}, sql.ErrConnDone) + store.EXPECT().CreateAccountTx(gomock.Any(), gomock.Any()).Times(1).Return(db.Account{}, sql.ErrConnDone) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusBadRequest, recorder.Code) @@ -207,7 +208,7 @@ func TestCreateAccountAPI(t *testing.T) { addAuth(t, request, tokenMaker, constants.AuthTypeBearer, user.Username, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { - store.EXPECT().CreateAccount(gomock.Any(), gomock.Any()).Times(0) + store.EXPECT().CreateAccountTx(gomock.Any(), gomock.Any()).Times(0) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusBadRequest, recorder.Code) diff --git a/api/controllers/account_controller.go b/api/controllers/account_controller.go index 61ddcd4..54af048 100644 --- a/api/controllers/account_controller.go +++ b/api/controllers/account_controller.go @@ -52,9 +52,10 @@ func (c *AccountController) CreateAccount(ctx *gin.Context) { Owner: authPayload.Username, Currency: req.Currency, Balance: 0, + AccType: db.AccountTypeBank, } - account, err := c.store.CreateAccount(ctx, arg) + account, err := c.store.CreateAccountTx(ctx, arg) if err != nil { if pqErr, ok := err.(*pq.Error); ok { switch pqErr.Code.Name() { From cd04a1cc55108404256b9ff383c793f4389fe32b Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 17:34:17 +0800 Subject: [PATCH 4/6] feat: deposit query --- db/mock/store.go | 45 +++++++++++++++++++++ db/query/account.sql | 13 ++++++ db/sqlc/account.sql.go | 53 +++++++++++++++++++++++++ db/sqlc/create_deposit_tx.go | 32 +++++++++++++++ db/sqlc/create_deposit_tx_test.go | 39 ++++++++++++++++++ db/sqlc/querier.go | 2 + db/sqlc/store.go | 66 +++++++++++++++++-------------- 7 files changed, 221 insertions(+), 29 deletions(-) create mode 100644 db/sqlc/create_deposit_tx.go create mode 100644 db/sqlc/create_deposit_tx_test.go diff --git a/db/mock/store.go b/db/mock/store.go index 39a5583..42a0298 100644 --- a/db/mock/store.go +++ b/db/mock/store.go @@ -80,6 +80,21 @@ func (mr *MockStoreMockRecorder) CreateAccountTx(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccountTx", reflect.TypeOf((*MockStore)(nil).CreateAccountTx), arg0, arg1) } +// CreateDepositTx mocks base method. +func (m *MockStore) CreateDepositTx(arg0 context.Context, arg1 db.CreateDepositTxParams) (db.Transfer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDepositTx", arg0, arg1) + ret0, _ := ret[0].(db.Transfer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDepositTx indicates an expected call of CreateDepositTx. +func (mr *MockStoreMockRecorder) CreateDepositTx(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDepositTx", reflect.TypeOf((*MockStore)(nil).CreateDepositTx), arg0, arg1) +} + // CreateEntry mocks base method. func (m *MockStore) CreateEntry(arg0 context.Context, arg1 db.CreateEntryParams) (db.Entry, error) { m.ctrl.T.Helper() @@ -154,6 +169,21 @@ func (mr *MockStoreMockRecorder) GetAccount(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1) } +// GetAccountByCurrencyForUpdate mocks base method. +func (m *MockStore) GetAccountByCurrencyForUpdate(arg0 context.Context, arg1 db.GetAccountByCurrencyForUpdateParams) (db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccountByCurrencyForUpdate", arg0, arg1) + ret0, _ := ret[0].(db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAccountByCurrencyForUpdate indicates an expected call of GetAccountByCurrencyForUpdate. +func (mr *MockStoreMockRecorder) GetAccountByCurrencyForUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByCurrencyForUpdate", reflect.TypeOf((*MockStore)(nil).GetAccountByCurrencyForUpdate), arg0, arg1) +} + // GetAccountForUpdate mocks base method. func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) { m.ctrl.T.Helper() @@ -199,6 +229,21 @@ func (mr *MockStoreMockRecorder) GetExtAccount(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtAccount", reflect.TypeOf((*MockStore)(nil).GetExtAccount), arg0, arg1) } +// GetExtAccountForUpdate mocks base method. +func (m *MockStore) GetExtAccountForUpdate(arg0 context.Context, arg1 db.GetExtAccountForUpdateParams) (db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetExtAccountForUpdate", arg0, arg1) + ret0, _ := ret[0].(db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetExtAccountForUpdate indicates an expected call of GetExtAccountForUpdate. +func (mr *MockStoreMockRecorder) GetExtAccountForUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExtAccountForUpdate", reflect.TypeOf((*MockStore)(nil).GetExtAccountForUpdate), arg0, arg1) +} + // GetTransfer mocks base method. func (m *MockStore) GetTransfer(arg0 context.Context, arg1 int64) (db.Transfer, error) { m.ctrl.T.Helper() diff --git a/db/query/account.sql b/db/query/account.sql index adc39c1..86f84e7 100644 --- a/db/query/account.sql +++ b/db/query/account.sql @@ -20,11 +20,24 @@ WHERE id = $1 LIMIT 1 FOR NO KEY UPDATE; +-- name: GetAccountByCurrencyForUpdate :one +SELECT * FROM accounts +WHERE currency = $1 AND owner = $2 + AND acc_type = 'bank' +LIMIT 1 +FOR NO KEY UPDATE; + -- name: GetExtAccount :one SELECT * FROM accounts WHERE owner = $1 AND currency = $2 AND acc_type = 'external' LIMIT 1; +-- name: GetExtAccountForUpdate :one +SELECT * FROM accounts +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +LIMIT 1 +FOR NO KEY UPDATE; + -- name: ListAccounts :many SELECT * FROM accounts WHERE owner = $1 diff --git a/db/sqlc/account.sql.go b/db/sqlc/account.sql.go index 988d8cc..40c34b2 100644 --- a/db/sqlc/account.sql.go +++ b/db/sqlc/account.sql.go @@ -102,6 +102,33 @@ func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) { return i, err } +const getAccountByCurrencyForUpdate = `-- name: GetAccountByCurrencyForUpdate :one +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE currency = $1 AND owner = $2 + AND acc_type = 'bank' +LIMIT 1 +FOR NO KEY UPDATE +` + +type GetAccountByCurrencyForUpdateParams struct { + Currency string `json:"currency"` + Owner string `json:"owner"` +} + +func (q *Queries) GetAccountByCurrencyForUpdate(ctx context.Context, arg GetAccountByCurrencyForUpdateParams) (Account, error) { + row := q.db.QueryRowContext(ctx, getAccountByCurrencyForUpdate, arg.Currency, arg.Owner) + var i Account + err := row.Scan( + &i.ID, + &i.Owner, + &i.Balance, + &i.Currency, + &i.CreatedAt, + &i.AccType, + ) + return i, err +} + const getAccountForUpdate = `-- name: GetAccountForUpdate :one SELECT id, owner, balance, currency, created_at, acc_type FROM accounts WHERE id = $1 @@ -149,6 +176,32 @@ func (q *Queries) GetExtAccount(ctx context.Context, arg GetExtAccountParams) (A return i, err } +const getExtAccountForUpdate = `-- name: GetExtAccountForUpdate :one +SELECT id, owner, balance, currency, created_at, acc_type FROM accounts +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +LIMIT 1 +FOR NO KEY UPDATE +` + +type GetExtAccountForUpdateParams struct { + Owner string `json:"owner"` + Currency string `json:"currency"` +} + +func (q *Queries) GetExtAccountForUpdate(ctx context.Context, arg GetExtAccountForUpdateParams) (Account, error) { + row := q.db.QueryRowContext(ctx, getExtAccountForUpdate, arg.Owner, arg.Currency) + var i Account + err := row.Scan( + &i.ID, + &i.Owner, + &i.Balance, + &i.Currency, + &i.CreatedAt, + &i.AccType, + ) + return i, err +} + const listAccounts = `-- name: ListAccounts :many SELECT id, owner, balance, currency, created_at, acc_type FROM accounts WHERE owner = $1 diff --git a/db/sqlc/create_deposit_tx.go b/db/sqlc/create_deposit_tx.go new file mode 100644 index 0000000..92a7f29 --- /dev/null +++ b/db/sqlc/create_deposit_tx.go @@ -0,0 +1,32 @@ +package db + +import "context" + +type CreateDepositTxParams struct { + User User + Currency string + Amount int64 `json:"amount"` +} + +func (store *SQLStore) CreateDepositTx(ctx context.Context, param CreateDepositTxParams) (Transfer, error) { + var res TransferTxResult + err := store.execTx(ctx, func(q *Queries) error { + var err_ error + + acc, err_ := q.GetAccountByCurrencyForUpdate(ctx, GetAccountByCurrencyForUpdateParams{Owner: param.User.Username, Currency: param.Currency}) + if err_ != nil { + return err_ + } + extacc, err_ := q.GetExtAccountForUpdate(ctx, GetExtAccountForUpdateParams{Owner: param.User.Username, Currency: param.Currency}) + if err_ != nil { + return err_ + } + res, err_ = TransferWithTx(q, ctx, TransferTxParams{FromAccountID: extacc.ID, ToAccountID: acc.ID, Amount: param.Amount}) + if err_ != nil { + return err_ + } + return nil + }) + + return res.Transfer, err +} diff --git a/db/sqlc/create_deposit_tx_test.go b/db/sqlc/create_deposit_tx_test.go new file mode 100644 index 0000000..c232e9e --- /dev/null +++ b/db/sqlc/create_deposit_tx_test.go @@ -0,0 +1,39 @@ +package db + +import ( + "context" + "testing" + + "github.com/hhow09/simple_bank/util" + "github.com/stretchr/testify/require" +) + +// create deposit: +// 1. create transfer +// 2. user's external account -= amount +// 3. user's bank account += ammount +func TestCreateDepositTx(t *testing.T) { + store := NewStore(testDB) + user := createRandomUser(t) + curr := util.RandomCurrency() + args := CreateAccountParams{ + Owner: user.Username, + Balance: util.RandomMoney(), + Currency: curr, + AccType: AccountTypeBank, + } + acc, err := store.CreateAccountTx(context.Background(), args) + require.NoError(t, err) + require.NotEmpty(t, acc) + amt := util.RandomMoney() + transfer, err := store.CreateDepositTx(context.Background(), CreateDepositTxParams{User: user, Currency: curr, Amount: amt}) + require.NoError(t, err) + require.NotEmpty(t, transfer) + require.Equal(t, transfer.Amount, amt) + updatedAcc, err := store.GetAccount(context.Background(), acc.ID) + require.NoError(t, err) + extAcc, err := store.GetExtAccount(context.Background(), GetExtAccountParams{Owner: user.Username, Currency: curr}) + require.NoError(t, err) + require.Equal(t, updatedAcc.Balance, acc.Balance+amt) + require.Equal(t, extAcc.Balance, 0-amt) +} diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 67834b5..6d2d20b 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -16,9 +16,11 @@ type Querier interface { CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteAccount(ctx context.Context, id int64) error GetAccount(ctx context.Context, id int64) (Account, error) + GetAccountByCurrencyForUpdate(ctx context.Context, arg GetAccountByCurrencyForUpdateParams) (Account, error) GetAccountForUpdate(ctx context.Context, id int64) (Account, error) GetEntry(ctx context.Context, id int64) (Entry, error) GetExtAccount(ctx context.Context, arg GetExtAccountParams) (Account, error) + GetExtAccountForUpdate(ctx context.Context, arg GetExtAccountForUpdateParams) (Account, error) GetTransfer(ctx context.Context, id int64) (Transfer, error) GetUser(ctx context.Context, username string) (User, error) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) diff --git a/db/sqlc/store.go b/db/sqlc/store.go index a9c8a21..7712935 100644 --- a/db/sqlc/store.go +++ b/db/sqlc/store.go @@ -13,6 +13,7 @@ type Store interface { Querier TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) CreateAccountTx(ctx context.Context, arg CreateAccountParams) (Account, error) + CreateDepositTx(ctx context.Context, param CreateDepositTxParams) (Transfer, error) } // SQLStore provides all funcs to execute queries and transactions @@ -83,39 +84,46 @@ func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (Tr err := store.execTx(ctx, func(q *Queries) error { var err error - result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ - FromAccountID: arg.FromAccountID, - ToAccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } - result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.FromAccountID, - Amount: -arg.Amount, - }) - if err != nil { - return err - } + result, err = TransferWithTx(q, ctx, arg) + return err + }) - result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } + return result, err +} - // update accounts' balance - if arg.FromAccountID < arg.ToAccountID { - result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) - } else { - result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) - } - return err +func TransferWithTx(q *Queries, ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { + var err error + var result TransferTxResult + result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ + FromAccountID: arg.FromAccountID, + ToAccountID: arg.ToAccountID, + Amount: arg.Amount, }) + if err != nil { + return result, err + } + result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.FromAccountID, + Amount: -arg.Amount, + }) + if err != nil { + return result, err + } + result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.ToAccountID, + Amount: arg.Amount, + }) + if err != nil { + return result, err + } + + // update accounts' balance + if arg.FromAccountID < arg.ToAccountID { + result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) + } else { + result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) + } return result, err } From f81a04ef95d2fdb272d705faf8fdfbe940057927 Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 17:35:03 +0800 Subject: [PATCH 5/6] feat: deposit service --- api/services/deposit_service.go | 44 +++++++++++++++++++++++++++++++++ api/services/services.go | 8 ++++++ 2 files changed, 52 insertions(+) create mode 100644 api/services/deposit_service.go create mode 100644 api/services/services.go diff --git a/api/services/deposit_service.go b/api/services/deposit_service.go new file mode 100644 index 0000000..00176f0 --- /dev/null +++ b/api/services/deposit_service.go @@ -0,0 +1,44 @@ +package services + +import ( + "context" + "database/sql" + "errors" + + db "github.com/hhow09/simple_bank/db/sqlc" + "github.com/hhow09/simple_bank/util" +) + +type DepositService struct { + store db.Store +} + +func NewDepositService(store db.Store) DepositService { + return DepositService{ + store: store, + } +} + +type DepositParams struct { + User db.User + Currency string + Amount int64 +} + +func (dc *DepositService) Deposit(ctx context.Context, params DepositParams) (db.Transfer, error) { + var transfer db.Transfer + if !util.IsSupportedCurrency(params.Currency) { + return transfer, errors.New("currency not supportted") + } + if params.Amount <= 0 { + return transfer, errors.New("deposit should be more than 0") + } + user, err := dc.store.GetUser(ctx, params.User.Username) + if err != nil { + if err == sql.ErrNoRows { + return transfer, errors.New("user not found") + } + return transfer, errors.New("internal server error") + } + return dc.store.CreateDepositTx(ctx, db.CreateDepositTxParams{User: user, Amount: params.Amount, Currency: params.Currency}) +} diff --git a/api/services/services.go b/api/services/services.go new file mode 100644 index 0000000..43033ab --- /dev/null +++ b/api/services/services.go @@ -0,0 +1,8 @@ +package services + +import "go.uber.org/fx" + +// Module exported for initializing application +var Module = fx.Options( + fx.Provide(NewDepositService), +) From f340f887447b3822be43a0372926a48012919676 Mon Sep 17 00:00:00 2001 From: hhow09 Date: Tue, 21 Mar 2023 17:39:23 +0800 Subject: [PATCH 6/6] refactor: add comments --- db/query/account.sql | 4 +++- db/sqlc/account.sql.go | 4 +++- db/sqlc/create_deposit_tx_test.go | 2 +- db/sqlc/querier.go | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/db/query/account.sql b/db/query/account.sql index 86f84e7..49667eb 100644 --- a/db/query/account.sql +++ b/db/query/account.sql @@ -28,13 +28,15 @@ LIMIT 1 FOR NO KEY UPDATE; -- name: GetExtAccount :one +-- for deposit use SELECT * FROM accounts WHERE owner = $1 AND currency = $2 AND acc_type = 'external' LIMIT 1; -- name: GetExtAccountForUpdate :one +-- for deposit use SELECT * FROM accounts -WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' LIMIT 1 FOR NO KEY UPDATE; diff --git a/db/sqlc/account.sql.go b/db/sqlc/account.sql.go index 40c34b2..abe11bd 100644 --- a/db/sqlc/account.sql.go +++ b/db/sqlc/account.sql.go @@ -162,6 +162,7 @@ type GetExtAccountParams struct { Currency string `json:"currency"` } +// for deposit use func (q *Queries) GetExtAccount(ctx context.Context, arg GetExtAccountParams) (Account, error) { row := q.db.QueryRowContext(ctx, getExtAccount, arg.Owner, arg.Currency) var i Account @@ -178,7 +179,7 @@ func (q *Queries) GetExtAccount(ctx context.Context, arg GetExtAccountParams) (A const getExtAccountForUpdate = `-- name: GetExtAccountForUpdate :one SELECT id, owner, balance, currency, created_at, acc_type FROM accounts -WHERE owner = $1 AND currency = $2 AND acc_type = 'external' +WHERE owner = $1 AND currency = $2 AND acc_type = 'external' LIMIT 1 FOR NO KEY UPDATE ` @@ -188,6 +189,7 @@ type GetExtAccountForUpdateParams struct { Currency string `json:"currency"` } +// for deposit use func (q *Queries) GetExtAccountForUpdate(ctx context.Context, arg GetExtAccountForUpdateParams) (Account, error) { row := q.db.QueryRowContext(ctx, getExtAccountForUpdate, arg.Owner, arg.Currency) var i Account diff --git a/db/sqlc/create_deposit_tx_test.go b/db/sqlc/create_deposit_tx_test.go index c232e9e..37710d4 100644 --- a/db/sqlc/create_deposit_tx_test.go +++ b/db/sqlc/create_deposit_tx_test.go @@ -11,7 +11,7 @@ import ( // create deposit: // 1. create transfer // 2. user's external account -= amount -// 3. user's bank account += ammount +// 3. user's bank account += amount func TestCreateDepositTx(t *testing.T) { store := NewStore(testDB) user := createRandomUser(t) diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 6d2d20b..2c4a017 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -19,7 +19,9 @@ type Querier interface { GetAccountByCurrencyForUpdate(ctx context.Context, arg GetAccountByCurrencyForUpdateParams) (Account, error) GetAccountForUpdate(ctx context.Context, id int64) (Account, error) GetEntry(ctx context.Context, id int64) (Entry, error) + // for deposit use GetExtAccount(ctx context.Context, arg GetExtAccountParams) (Account, error) + // for deposit use GetExtAccountForUpdate(ctx context.Context, arg GetExtAccountForUpdateParams) (Account, error) GetTransfer(ctx context.Context, id int64) (Transfer, error) GetUser(ctx context.Context, username string) (User, error)