1package main
2
3import (
4	"fmt"
5	"time"
6
7	"github.com/golang/protobuf/ptypes"
8	"github.com/golang/protobuf/ptypes/empty"
9	"golang.org/x/net/context"
10	"google.golang.org/grpc/codes"
11	"google.golang.org/grpc/status"
12)
13
14// bankServer implements the Bank gRPC service.
15type bankServer struct {
16	allAccounts *accounts
17}
18
19func (s *bankServer) OpenAccount(ctx context.Context, req *OpenAccountRequest) (*Account, error) {
20	cust := getCustomer(ctx)
21	if cust == "" {
22		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
23	}
24	switch req.Type {
25	case Account_CHECKING, Account_SAVING, Account_MONEY_MARKET:
26		if req.InitialDepositCents < 0 {
27			return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount cannot be negative: %s", dollars(req.InitialDepositCents))
28		}
29	case Account_LINE_OF_CREDIT, Account_LOAN, Account_EQUITIES:
30		if req.InitialDepositCents != 0 {
31			return nil, status.Errorf(codes.InvalidArgument, "initial deposit amount must be zero for account type %v: %s", req.Type, dollars(req.InitialDepositCents))
32		}
33	default:
34		return nil, status.Errorf(codes.InvalidArgument, "invalid account type: %v", req.Type)
35	}
36
37	return s.allAccounts.openAccount(cust, req.Type, req.InitialDepositCents), nil
38}
39
40func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest) (*empty.Empty, error) {
41	cust := getCustomer(ctx)
42	if cust == "" {
43		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
44	}
45
46	if err := s.allAccounts.closeAccount(cust, req.AccountNumber); err != nil {
47		return nil, err
48	}
49	return &empty.Empty{}, nil
50}
51
52func (s *bankServer) GetAccounts(ctx context.Context, _ *empty.Empty) (*GetAccountsResponse, error) {
53	cust := getCustomer(ctx)
54	if cust == "" {
55		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
56	}
57
58	accounts := s.allAccounts.getAllAccounts(cust)
59	return &GetAccountsResponse{Accounts: accounts}, nil
60}
61
62func (s *bankServer) GetTransactions(req *GetTransactionsRequest, stream Bank_GetTransactionsServer) error {
63	cust := getCustomer(stream.Context())
64	if cust == "" {
65		return status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
66	}
67
68	acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
69	if err != nil {
70		return err
71	}
72
73	var start, end time.Time
74	if req.Start != nil {
75		start, err = ptypes.Timestamp(req.Start)
76		if err != nil {
77			return err
78		}
79	}
80	if req.End != nil {
81		end, err = ptypes.Timestamp(req.End)
82		if err != nil {
83			return err
84		}
85	} else {
86		end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.Local)
87	}
88
89	txns := acct.getTransactions()
90	for _, txn := range txns {
91		t, err := ptypes.Timestamp(txn.Date)
92		if err != nil {
93			return err
94		}
95		if (t.After(start) || t.Equal(start)) &&
96			(t.Before(end) || t.Equal(end)) {
97
98			if err := stream.Send(txn); err != nil {
99				return err
100			}
101		}
102	}
103	return nil
104}
105
106func (s *bankServer) Deposit(ctx context.Context, req *DepositRequest) (*BalanceResponse, error) {
107	cust := getCustomer(ctx)
108	if cust == "" {
109		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
110	}
111
112	switch req.Source {
113	case DepositRequest_ACH, DepositRequest_CASH, DepositRequest_CHECK, DepositRequest_WIRE:
114		// ok
115	default:
116		return nil, status.Errorf(codes.InvalidArgument, "unknown deposit source: %v", req.Source)
117	}
118
119	if req.AmountCents <= 0 {
120		return nil, status.Errorf(codes.InvalidArgument, "deposit amount cannot be non-positive: %s", dollars(req.AmountCents))
121	}
122
123	desc := fmt.Sprintf("%v deposit", req.Source)
124	if req.Desc != "" {
125		desc = fmt.Sprintf("%s: %s", desc, req.Desc)
126	}
127	acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
128	if err != nil {
129		return nil, err
130	}
131	newBalance, err := acct.newTransaction(req.AmountCents, desc)
132	if err != nil {
133		return nil, err
134	}
135	return &BalanceResponse{
136		AccountNumber: req.AccountNumber,
137		BalanceCents:  newBalance,
138	}, nil
139}
140
141func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*BalanceResponse, error) {
142	cust := getCustomer(ctx)
143	if cust == "" {
144		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
145	}
146
147	if req.AmountCents >= 0 {
148		return nil, status.Errorf(codes.InvalidArgument, "withdrawal amount cannot be non-negative: %s", dollars(req.AmountCents))
149	}
150
151	acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
152	if err != nil {
153		return nil, err
154	}
155	newBalance, err := acct.newTransaction(req.AmountCents, req.Desc)
156	if err != nil {
157		return nil, err
158	}
159	return &BalanceResponse{
160		AccountNumber: req.AccountNumber,
161		BalanceCents:  newBalance,
162	}, nil
163}
164
165func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) {
166	cust := getCustomer(ctx)
167	if cust == "" {
168		return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
169	}
170
171	if req.AmountCents <= 0 {
172		return nil, status.Errorf(codes.InvalidArgument, "transfer amount cannot be non-positive: %s", dollars(req.AmountCents))
173	}
174
175	var srcAcct *account
176	var srcDesc string
177	switch src := req.Source.(type) {
178	case *TransferRequest_ExternalSource:
179		srcDesc = fmt.Sprintf("ACH %09d:%06d", src.ExternalSource.AchRoutingNumber, src.ExternalSource.AchAccountNumber)
180		if src.ExternalSource.AchAccountNumber == 0 || src.ExternalSource.AchRoutingNumber == 0 {
181			return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", srcDesc)
182		}
183	case *TransferRequest_SourceAccountNumber:
184		srcDesc = fmt.Sprintf("account %06d", src.SourceAccountNumber)
185		var err error
186		if srcAcct, err = s.allAccounts.getAccount(cust, src.SourceAccountNumber); err != nil {
187			return nil, err
188		}
189	}
190
191	var destAcct *account
192	var destDesc string
193	switch dest := req.Dest.(type) {
194	case *TransferRequest_ExternalDest:
195		destDesc = fmt.Sprintf("ACH %09d:%06d", dest.ExternalDest.AchRoutingNumber, dest.ExternalDest.AchAccountNumber)
196		if dest.ExternalDest.AchAccountNumber == 0 || dest.ExternalDest.AchRoutingNumber == 0 {
197			return nil, status.Errorf(codes.InvalidArgument, "external source routing and account numbers cannot be zero: %s", destDesc)
198		}
199	case *TransferRequest_DestAccountNumber:
200		destDesc = fmt.Sprintf("account %06d", dest.DestAccountNumber)
201		var err error
202		if destAcct, err = s.allAccounts.getAccount(cust, dest.DestAccountNumber); err != nil {
203			return nil, err
204		}
205	}
206
207	var srcBalance int32
208	if srcAcct != nil {
209		desc := fmt.Sprintf("transfer to %s", destDesc)
210		if req.Desc != "" {
211			desc = fmt.Sprintf("%s: %s", desc, req.Desc)
212		}
213		var err error
214		if srcBalance, err = srcAcct.newTransaction(-req.AmountCents, desc); err != nil {
215			return nil, err
216		}
217	}
218
219	var destBalance int32
220	if destAcct != nil {
221		desc := fmt.Sprintf("transfer from %s", srcDesc)
222		if req.Desc != "" {
223			desc = fmt.Sprintf("%s: %s", desc, req.Desc)
224		}
225		var err error
226		if destBalance, err = destAcct.newTransaction(req.AmountCents, desc); err != nil {
227			return nil, err
228		}
229	}
230
231	return &TransferResponse{
232		SrcAccountNumber:  req.GetSourceAccountNumber(),
233		SrcBalanceCents:   srcBalance,
234		DestAccountNumber: req.GetDestAccountNumber(),
235		DestBalanceCents:  destBalance,
236	}, nil
237}
238