grpcurl/testing/cmd/bankdemo/bank.go

320 lines
9.8 KiB
Go

package main
import (
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// bankServer implements the Bank gRPC service.
type bankServer struct {
allAccounts *accounts
}
func (s *bankServer) OpenAccount(ctx context.Context, req *OpenAccountRequest) (*Account, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
switch req.Type {
case Account_CHECKING, Account_SAVING, Account_MONEY_MARKET:
if req.InitialDepositCents < 0 {
return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount cannot be negative: %s", dollars(req.InitialDepositCents))
}
case Account_LINE_OF_CREDIT, Account_LOAN, Account_EQUITIES:
if req.InitialDepositCents != 0 {
return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount must be zero for account type %v: %s", req.Type, dollars(req.InitialDepositCents))
}
default:
return nil, status.Errorf(codes.InvalidArgument, "invalid account type: %v", req.Type)
}
s.allAccounts.mu.Lock()
defer s.allAccounts.mu.Unlock()
accountNums, ok := s.allAccounts.AccountNumbersByCustomer[cust]
if !ok {
// no accounts for this customer? it's a new customer
s.allAccounts.Customers = append(s.allAccounts.Customers, cust)
}
num := s.allAccounts.LastAccountNum + 1
s.allAccounts.LastAccountNum = num
s.allAccounts.AccountNumbers = append(s.allAccounts.AccountNumbers, num)
accountNums = append(accountNums, num)
s.allAccounts.AccountNumbersByCustomer[cust] = accountNums
var acct account
acct.AccountNumber = num
acct.BalanceCents = req.InitialDepositCents
acct.Transactions = append(acct.Transactions, &Transaction{
AccountNumber: num,
SeqNumber: 1,
Date: ptypes.TimestampNow(),
AmountCents: req.InitialDepositCents,
Desc: "initial deposit",
})
s.allAccounts.AccountsByNumber[num] = &acct
return &acct.Account, nil
}
func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest) (*empty.Empty, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
s.allAccounts.mu.Lock()
defer s.allAccounts.mu.Unlock()
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
found := -1
for i, num := range acctNums {
if num == req.AccountNumber {
found = i
break
}
}
if found == -1 {
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", req.AccountNumber)
}
for i, num := range s.allAccounts.AccountNumbers {
if num == req.AccountNumber {
s.allAccounts.AccountNumbers = append(s.allAccounts.AccountNumbers[:i], s.allAccounts.AccountNumbers[i+1:]...)
break
}
}
acct := s.allAccounts.AccountsByNumber[req.AccountNumber]
if acct.BalanceCents != 0 {
return nil, status.Errorf(codes.FailedPrecondition, "account %d cannot be closed because it has a non-zero balance: %s", req.AccountNumber, dollars(acct.BalanceCents))
}
s.allAccounts.AccountNumbersByCustomer[cust] = append(acctNums[:found], acctNums[found+1:]...)
delete(s.allAccounts.AccountsByNumber, req.AccountNumber)
return &empty.Empty{}, nil
}
func (s *bankServer) GetAccounts(ctx context.Context, _ *empty.Empty) (*GetAccountsResponse, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
s.allAccounts.mu.RLock()
defer s.allAccounts.mu.RUnlock()
accountNums := s.allAccounts.AccountNumbersByCustomer[cust]
var accounts []*Account
for _, num := range accountNums {
accounts = append(accounts, &s.allAccounts.AccountsByNumber[num].Account)
}
return &GetAccountsResponse{Accounts: accounts}, nil
}
func (s *bankServer) GetTransactions(req *GetTransactionsRequest, stream Bank_GetTransactionsServer) error {
cust := getCustomer(stream.Context())
if cust == "" {
return status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
acct, err := func() (*account, error) {
s.allAccounts.mu.Lock()
defer s.allAccounts.mu.Unlock()
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
for _, num := range acctNums {
if num == req.AccountNumber {
return s.allAccounts.AccountsByNumber[num], nil
}
}
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", req.AccountNumber)
}()
if err != nil {
return err
}
var start, end time.Time
if req.Start != nil {
start, err = ptypes.Timestamp(req.Start)
if err != nil {
return err
}
}
if req.End != nil {
end, err = ptypes.Timestamp(req.End)
if err != nil {
return err
}
} else {
end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.Local)
}
acct.mu.RLock()
txns := acct.Transactions
acct.mu.RUnlock()
for _, txn := range txns {
t, err := ptypes.Timestamp(txn.Date)
if err != nil {
return err
}
if (t.After(start) || t.Equal(start)) &&
(t.Before(end) || t.Equal(end)) {
if err := stream.Send(txn); err != nil {
return err
}
}
}
return nil
}
func (s *bankServer) Deposit(ctx context.Context, req *DepositRequest) (*BalanceResponse, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
switch req.Source {
case DepositRequest_ACH, DepositRequest_CASH, DepositRequest_CHECK, DepositRequest_WIRE:
// ok
default:
return nil, status.Errorf(codes.InvalidArgument, "unknown deposit source: %v", req.Source)
}
if req.AmountCents <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "deposit amount cannot be non-positive: %s", dollars(req.AmountCents))
}
desc := fmt.Sprintf("%v deposit", req.Source)
if req.Desc != "" {
desc = fmt.Sprintf("%s: %s", desc, req.Desc)
}
newBalance, err := s.newTransaction(cust, req.AccountNumber, req.AmountCents, desc)
if err != nil {
return nil, err
}
return &BalanceResponse{
AccountNumber: req.AccountNumber,
BalanceCents: newBalance,
}, nil
}
func (s *bankServer) getAccount(cust string, acctNumber uint64) (*account, error) {
s.allAccounts.mu.Lock()
defer s.allAccounts.mu.Unlock()
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
for _, num := range acctNums {
if num == acctNumber {
return s.allAccounts.AccountsByNumber[num], nil
}
}
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", acctNumber)
}
func (s *bankServer) newTransaction(cust string, acctNumber uint64, amountCents int32, desc string) (int32, error) {
acct, err := s.getAccount(cust, acctNumber)
if err != nil {
return 0, err
}
return acct.newTransaction(amountCents, desc)
}
func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*BalanceResponse, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
if req.AmountCents >= 0 {
return nil, status.Errorf(codes.InvalidArgument, "withdrawal amount cannot be non-negative: %s", dollars(req.AmountCents))
}
newBalance, err := s.newTransaction(cust, req.AccountNumber, req.AmountCents, req.Desc)
if err != nil {
return nil, err
}
return &BalanceResponse{
AccountNumber: req.AccountNumber,
BalanceCents: newBalance,
}, nil
}
func dollars(amountCents int32) string {
return fmt.Sprintf("$%02f", float64(amountCents)/100)
}
func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) {
cust := getCustomer(ctx)
if cust == "" {
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
}
if req.AmountCents <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "transfer amount cannot be non-positive: %s", dollars(req.AmountCents))
}
var srcAcct *account
var srcDesc string
switch src := req.Source.(type) {
case *TransferRequest_ExternalSource:
srcDesc = fmt.Sprintf("ACH %09d:%06d", src.ExternalSource.AchRoutingNumber, src.ExternalSource.AchAccountNumber)
if src.ExternalSource.AchAccountNumber == 0 || src.ExternalSource.AchRoutingNumber == 0 {
return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", srcDesc)
}
case *TransferRequest_SourceAccountNumber:
srcDesc = fmt.Sprintf("account %06d", src.SourceAccountNumber)
var err error
if srcAcct, err = s.getAccount(cust, src.SourceAccountNumber); err != nil {
return nil, err
}
}
var destAcct *account
var destDesc string
switch dest := req.Dest.(type) {
case *TransferRequest_ExternalDest:
destDesc = fmt.Sprintf("ACH %09d:%06d", dest.ExternalDest.AchRoutingNumber, dest.ExternalDest.AchAccountNumber)
if dest.ExternalDest.AchAccountNumber == 0 || dest.ExternalDest.AchRoutingNumber == 0 {
return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", destDesc)
}
case *TransferRequest_DestAccountNumber:
destDesc = fmt.Sprintf("account %06d", dest.DestAccountNumber)
var err error
if destAcct, err = s.getAccount(cust, dest.DestAccountNumber); err != nil {
return nil, err
}
}
var srcBalance int32
if srcAcct != nil {
desc := fmt.Sprintf("transfer to %s", destDesc)
if req.Desc != "" {
desc = fmt.Sprintf("%s: %s", desc, req.Desc)
}
var err error
if srcBalance, err = srcAcct.newTransaction(-req.AmountCents, desc); err != nil {
return nil, err
}
}
var destBalance int32
if destAcct != nil {
desc := fmt.Sprintf("transfer from %s", srcDesc)
if req.Desc != "" {
desc = fmt.Sprintf("%s: %s", desc, req.Desc)
}
var err error
if destBalance, err = destAcct.newTransaction(req.AmountCents, desc); err != nil {
return nil, err
}
}
return &TransferResponse{
SrcAccountNumber: req.GetSourceAccountNumber(),
SrcBalanceCents: srcBalance,
DestAccountNumber: req.GetDestAccountNumber(),
DestBalanceCents: destBalance,
}, nil
}