Skip to content

Commit 05da697

Browse files
feature: account transfers (#24) (#26)
This closes #21 and allows customers (and owners) to transfer money between their own accounts freely.
1 parent 60e13f1 commit 05da697

File tree

10 files changed

+316
-171
lines changed

10 files changed

+316
-171
lines changed

internal/api/handler/customer_handler.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,36 @@ package handler
33
import (
44
"fmt"
55
"net/http"
6+
"strconv"
67
"strings"
78

89
"github.com/bytebury/fun-banking/internal/domain"
10+
"github.com/bytebury/fun-banking/internal/infrastructure/persistence"
911
"github.com/bytebury/fun-banking/internal/service"
1012
"github.com/gin-gonic/gin"
1113
)
1214

1315
type customerHandler struct {
1416
pageObject
15-
bankService service.BankService
16-
customerService service.CustomerService
17-
accountService service.AccountService
18-
userService service.UserService
19-
Bank domain.Bank
20-
Customer domain.Customer
17+
bankService service.BankService
18+
customerService service.CustomerService
19+
accountService service.AccountService
20+
transactionService service.TransactionService
21+
userService service.UserService
22+
Bank domain.Bank
23+
Customer domain.Customer
24+
MAX_TRANSACTION_AMOUNT int
2125
}
2226

2327
func NewCustomerHandler() customerHandler {
2428
return customerHandler{
25-
bankService: service.NewBankService(),
26-
customerService: service.NewCustomerService(),
27-
userService: service.NewUserService(),
28-
accountService: service.NewAccountService(),
29-
Bank: domain.Bank{},
29+
bankService: service.NewBankService(),
30+
customerService: service.NewCustomerService(),
31+
userService: service.NewUserService(),
32+
accountService: service.NewAccountService(),
33+
transactionService: service.NewTransactionService(),
34+
Bank: domain.Bank{},
35+
MAX_TRANSACTION_AMOUNT: domain.MAX_TRANSACTION_AMOUNT,
3036
}
3137
}
3238

@@ -165,6 +171,76 @@ func (h customerHandler) OpenAccount(c *gin.Context) {
165171
c.Header("HX-Redirect", fmt.Sprintf("/accounts/%d", account.ID))
166172
}
167173

174+
func (h customerHandler) OpenTransferMoneyModal(c *gin.Context) {
175+
h.Reset(c)
176+
h.ModalType = "transfer_money_modal"
177+
178+
if err := h.customerService.FindByID(c.Param("id"), &h.Customer); err != nil {
179+
c.HTML(http.StatusNotFound, "not-found", nil)
180+
return
181+
}
182+
183+
c.HTML(http.StatusOK, "modal", h)
184+
}
185+
186+
func (h customerHandler) TransferMoney(c *gin.Context) {
187+
h.Reset(c)
188+
189+
if err := h.customerService.FindByID(c.Param("id"), &h.Customer); err != nil {
190+
h.Form.Errors["general"] = "Could not find customer"
191+
c.HTML(http.StatusNotFound, "account/transfer-money-form", h)
192+
return
193+
}
194+
195+
var fromAccount domain.Account
196+
var toAccount domain.Account
197+
198+
if err := persistence.DB.First(&fromAccount, "id = ?", h.Form.Data["from_account"]).Error; err != nil {
199+
h.Form.Errors["general"] = "Account does not exist"
200+
c.HTML(http.StatusNotFound, "account/transfer-money-form", h)
201+
return
202+
}
203+
204+
if err := persistence.DB.First(&toAccount, "id = ?", h.Form.Data["to_account"]).Error; err != nil {
205+
h.Form.Errors["general"] = "Account does not exist"
206+
c.HTML(http.StatusNotFound, "account/transfer_money_form", h)
207+
return
208+
}
209+
210+
amount, err := strconv.ParseFloat(h.Form.Data["amount"], 64)
211+
212+
if err != nil {
213+
h.Form.Errors["amount"] = "Invalid currency value"
214+
c.HTML(http.StatusUnprocessableEntity, "account/transfer_money_form", h)
215+
}
216+
217+
if err := h.transactionService.TransferMoney(fromAccount, toAccount, amount); err != nil {
218+
switch err := err.Error(); err {
219+
case "not enough money":
220+
h.Form.Errors["from_account"] = "You do not have enough money in this account"
221+
c.HTML(http.StatusUnprocessableEntity, "account/transfer_money_form", h)
222+
return
223+
case "cannot transfer to same account":
224+
h.Form.Errors["to_account"] = "You cannot transfer money to the same account"
225+
c.HTML(http.StatusUnprocessableEntity, "account/transfer_money_form", h)
226+
return
227+
case "cannot transfer to other customers accounts":
228+
h.Form.Errors["general"] = "You do not have enough money in this account"
229+
c.HTML(http.StatusUnprocessableEntity, "account/transfer_money_form", h)
230+
return
231+
}
232+
}
233+
234+
if err := h.customerService.FindByID(c.Param("id"), &h.Customer); err != nil {
235+
h.Form.Errors["general"] = "Could not find customer"
236+
c.HTML(http.StatusNotFound, "account/transfer-money-form", h)
237+
return
238+
}
239+
240+
c.Header("HX-Trigger", "closeModal")
241+
c.HTML(http.StatusOK, "account/transfer_money_oob", h)
242+
}
243+
168244
func (h customerHandler) isOwner(customerID, userID string) bool {
169245
var customer domain.Customer
170246
if err := h.customerService.FindByID(customerID, &customer); err != nil {

internal/api/routes.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"fmt"
45
"html/template"
56
"net/http"
67
"os"
@@ -34,6 +35,7 @@ func Start() {
3435
"mulfloat": func(a, b float64) float64 { return a * b },
3536
"datetime": func(dateTime time.Time) string { return dateTime.Format("January 02, 2006 at 3:04 PM") },
3637
"date": func(date time.Time) string { return date.Format("January 02, 2006") },
38+
"streq": func(a, b interface{}) bool { return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) },
3739
})
3840
// Load Templates
3941
router.LoadHTMLGlob("templates/**/*")
@@ -137,7 +139,9 @@ func setupCustomerRoutes() {
137139
DELETE(":id", middleware.UserAuth(), handler.Delete).
138140
GET(":id/open-account", middleware.UserAuth(), handler.OpenAccountModal).
139141
PUT(":id/open-account", middleware.UserAuth(), handler.OpenAccount).
140-
POST(":id/settings", middleware.UserAuth(), handler.OpenSettingsModal)
142+
POST(":id/settings", middleware.UserAuth(), handler.OpenSettingsModal).
143+
POST(":id/open-transfer-modal", middleware.AnyAuth(), handler.OpenTransferMoneyModal).
144+
PUT(":id/transfer", middleware.AnyAuth(), handler.TransferMoney)
141145
}
142146

143147
func setupAccountRoutes() {

internal/domain/transaction.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
)
88

99
const (
10-
TransactionPending = "PENDING"
11-
TransactionApproved = "APPROVED"
12-
TransactionDeclined = "DECLINED"
10+
TransactionPending = "PENDING"
11+
TransactionApproved = "APPROVED"
12+
TransactionDeclined = "DECLINED"
13+
MAX_TRANSACTION_AMOUNT = 25_000_000
1314
)
1415

1516
type Transaction struct {
@@ -41,7 +42,7 @@ func (t *Transaction) BeforeCreate(tx *gorm.DB) error {
4142
return errors.New("amount cannot be 0")
4243
}
4344

44-
if t.Amount > 25_000_000 {
45+
if t.Amount > MAX_TRANSACTION_AMOUNT {
4546
return errors.New("amount cannot be greater than 25,000,000")
4647
}
4748

internal/service/transaction_service.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Cashflow struct {
2020
type TransactionService interface {
2121
Create(transaction *domain.Transaction) error
2222
Update(id, userID, status string) error
23+
TransferMoney(from domain.Account, to domain.Account, amount float64) error
2324
SendMoney(fromAccount domain.Account, recipient domain.Customer, transaction *domain.Transaction) error
2425
BulkTransfer(customerIDs []string, transaction *domain.Transaction) error
2526
AutoPay(autoPay domain.AutoPay) error
@@ -112,6 +113,56 @@ func (ts transactionService) Create(transaction *domain.Transaction) error {
112113
})
113114
}
114115

116+
func (s transactionService) TransferMoney(from domain.Account, to domain.Account, amount float64) error {
117+
return persistence.DB.Transaction(func(tx *gorm.DB) error {
118+
if from.Balance < amount {
119+
return errors.New("not enough money")
120+
}
121+
122+
if from.ID == to.ID {
123+
return errors.New("cannot transfer to same account")
124+
}
125+
126+
if from.CustomerID != to.CustomerID {
127+
return errors.New("cannot transfer to other customers accounts")
128+
}
129+
130+
fromTransaction := domain.Transaction{}
131+
132+
from.Balance = utils.SafelySubtractDollars(from.Balance, amount)
133+
134+
fromTransaction.Amount = amount * -1
135+
fromTransaction.Balance = from.Balance
136+
fromTransaction.AccountID = from.ID
137+
fromTransaction.Description = fmt.Sprintf("Money transfer to %s", to.Name)
138+
fromTransaction.Status = domain.TransactionApproved
139+
140+
if err := s.accountService.UpdateBalance(strconv.Itoa(from.ID), &from); err != nil {
141+
return err
142+
}
143+
144+
if err := persistence.DB.Create(&fromTransaction).Error; err != nil {
145+
return err
146+
}
147+
148+
toTransaction := domain.Transaction{}
149+
150+
to.Balance = utils.SafelyAddDollars(to.Balance, amount)
151+
152+
toTransaction.Amount = amount
153+
toTransaction.Balance = to.Balance
154+
toTransaction.AccountID = to.ID
155+
toTransaction.Description = fmt.Sprintf("Money transfer from %s", from.Name)
156+
toTransaction.Status = domain.TransactionApproved
157+
158+
if err := s.accountService.UpdateBalance(strconv.Itoa(to.ID), &to); err != nil {
159+
return err
160+
}
161+
162+
return persistence.DB.Create(&toTransaction).Error
163+
})
164+
}
165+
115166
func (s transactionService) SendMoney(fromAccount domain.Account, recipient domain.Customer, transaction *domain.Transaction) error {
116167
return persistence.DB.Transaction(func(tx *gorm.DB) error {
117168
if fromAccount.Balance < transaction.Amount {

0 commit comments

Comments
 (0)