diff --git a/internal/howmuch/adapter/controller/session.go b/internal/howmuch/adapter/controller/session.go index 08ddb5d..27db01a 100644 --- a/internal/howmuch/adapter/controller/session.go +++ b/internal/howmuch/adapter/controller/session.go @@ -64,7 +64,7 @@ type createParams struct { // Since we use JWT method, this token is not stored anywhere. Thus it // stops at the controller level. func (sc *SessionController) Create(ctx *gin.Context) { - var user model.UserExistDTO + var user model.UserExistRequest if err := ctx.Bind(&user); err != nil { log.ErrorLog("param error", "err", err) diff --git a/internal/howmuch/adapter/controller/usecasemock/testuserusecase.go b/internal/howmuch/adapter/controller/usecasemock/testuserusecase.go index 21e0d6d..bfdb0e2 100644 --- a/internal/howmuch/adapter/controller/usecasemock/testuserusecase.go +++ b/internal/howmuch/adapter/controller/usecasemock/testuserusecase.go @@ -37,12 +37,12 @@ func NewtestUserUsecase() usecase.User { func (*testUserUsecase) Create( ctx context.Context, - u *model.UserCreateDTO, -) (*model.UserInfoVO, error) { + u *model.UserCreateRequest, +) (*model.UserInfoResponse, error) { return nil, nil } -func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error { +func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error { switch u.Email { case "a@b.c": if u.Password == "strong password" { @@ -57,10 +57,10 @@ func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error return nil } -func (*testUserUsecase) GetUserBaseVOByID( +func (*testUserUsecase) GetUserBaseResponseByID( ctx context.Context, userID int, -) (*model.UserBaseVO, error) { +) (*model.UserBaseResponse, error) { // TODO: return nil, nil } diff --git a/internal/howmuch/adapter/controller/user.go b/internal/howmuch/adapter/controller/user.go index cab830e..f6f434f 100644 --- a/internal/howmuch/adapter/controller/user.go +++ b/internal/howmuch/adapter/controller/user.go @@ -57,14 +57,14 @@ func NewUserController(us usecase.User) User { } func (uc *UserController) Create(ctx core.Context) { - var userDTO model.UserCreateDTO + var userRequest model.UserCreateRequest - if err := ctx.Bind(&userDTO); err != nil { + if err := ctx.Bind(&userRequest); err != nil { core.WriteResponse(ctx, UserParamsErr, nil) return } - _, err := uc.userUsecase.Create(ctx, &userDTO) + _, err := uc.userUsecase.Create(ctx, &userRequest) if err != nil { core.WriteResponse(ctx, err, nil) return diff --git a/internal/howmuch/adapter/repo/user.go b/internal/howmuch/adapter/repo/user.go index 8b6465b..405faec 100644 --- a/internal/howmuch/adapter/repo/user.go +++ b/internal/howmuch/adapter/repo/user.go @@ -50,8 +50,8 @@ func NewUserRepository(db *sql.DB) repo.UserRepository { func (ur *userRepository) Create( ctx context.Context, transaction interface{}, - u *model.UserPO, -) (*model.UserPO, error) { + u *model.UserEntity , +) (*model.UserEntity , error) { timeoutCtx, cancel := context.WithTimeout(ctx, insertTimeout) defer cancel() @@ -76,7 +76,7 @@ func (ur *userRepository) Create( return nil, err } - return &model.UserPO{ + return &model.UserEntity { ID: int(userDB.ID), Email: userDB.Email, FirstName: userDB.FirstName, @@ -88,7 +88,7 @@ func (ur *userRepository) Create( } // GetByEmail if not found, return nil for user but not error. -func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.UserPO, error) { +func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.UserEntity , error) { queries := sqlc.New(ur.db) userDB, err := queries.GetUserByEmail(ctx, email) if errors.Is(err, pgx.ErrNoRows) { @@ -98,7 +98,7 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model. return nil, err } - return &model.UserPO{ + return &model.UserEntity { ID: int(userDB.ID), Email: userDB.Email, FirstName: userDB.FirstName, @@ -109,7 +109,7 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model. }, nil } -func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserPO, error) { +func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserEntity , error) { queries := sqlc.New(ur.db) userDB, err := queries.GetUserByID(ctx, int32(id)) if errors.Is(err, pgx.ErrNoRows) { @@ -119,7 +119,7 @@ func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserPO, e return nil, err } - return &model.UserPO{ + return &model.UserEntity { ID: int(userDB.ID), Email: userDB.Email, FirstName: userDB.FirstName, diff --git a/internal/howmuch/model/event.go b/internal/howmuch/model/event.go index ce4815c..3737d67 100644 --- a/internal/howmuch/model/event.go +++ b/internal/howmuch/model/event.go @@ -24,9 +24,9 @@ package model import "time" -// {{{ DTO Data Transfer Object (from controller to service) +// {{{ Request Object (from controller to service) -type EventCreateDTO struct { +type EventCreateRequest struct { Name string `json:"name" binding:"requiered"` Description string `json:"description"` OwnerID int `json:"owner_id" binding:"requiered,number"` @@ -34,9 +34,17 @@ type EventCreateDTO struct { } // }}} -// {{{ VO View Object (from service to controller) +// {{{ Response View Object (from service to controller) -type EventInfoVO struct { +type EventBaseItemResponse struct { + ID int + Name string + Description string + Owner *UserBaseResponse + CreatedAt time.Time +} + +type EventInfoResponse struct { ID int Name string @@ -44,18 +52,26 @@ type EventInfoVO struct { TotalAmount Money - Owner *UserBaseVO + Owner *UserBaseResponse CreatedAt time.Time UpdatedAt time.Time - Users []*UserBaseVO + Users []*UserBaseResponse } // }}} -// {{{ PO Persistant Object (Between the service and the repository) +// {{{ Entity Persistant Object (Between the service and the repository) -type EventPO struct { +type EventBaseItemEntity struct { + ID int + Name string + Description string + OwnerID int + CreatedAt time.Time +} + +type EventEntity struct { ID int Name string diff --git a/internal/howmuch/model/expense.go b/internal/howmuch/model/expense.go index 6f6c091..1053f29 100644 --- a/internal/howmuch/model/expense.go +++ b/internal/howmuch/model/expense.go @@ -24,7 +24,7 @@ package model import "time" -type ExpenseDTO struct { +type ExpenseRequest struct { Amount Money `json:"money" binding:"required,number"` PayerIDs []int `json:"payer_ids" binding:"required"` RecipientIDs []int `json:"recipient_ids" binding:"required"` @@ -32,7 +32,7 @@ type ExpenseDTO struct { Detail ExpenseDetail `json:"detail"` } -type ExpensePO struct { +type ExpenseEntity struct { ID int Amount int diff --git a/internal/howmuch/model/participation.go b/internal/howmuch/model/participation.go index c2a25b4..747ca4c 100644 --- a/internal/howmuch/model/participation.go +++ b/internal/howmuch/model/participation.go @@ -24,7 +24,7 @@ package model import "time" -type ParticipationPO Participation +type ParticipationEntity Participation // Participation is the association between Users and Events type Participation struct { diff --git a/internal/howmuch/model/transaction.go b/internal/howmuch/model/transaction.go index 31ec8f5..f49d493 100644 --- a/internal/howmuch/model/transaction.go +++ b/internal/howmuch/model/transaction.go @@ -24,7 +24,7 @@ package model import "time" -type TransactionPO Transaction +type TransactionEntity Transaction // Transaction is the association between Expenses and Users type Transaction struct { diff --git a/internal/howmuch/model/user.go b/internal/howmuch/model/user.go index e74ea19..74d62d6 100644 --- a/internal/howmuch/model/user.go +++ b/internal/howmuch/model/user.go @@ -24,31 +24,31 @@ package model import "time" -// {{{ DTO Data Transfer Object (from controller to service) +// {{{ Request (from controller to service) -type UserCreateDTO struct { +type UserCreateRequest struct { Email string `json:"email" binding:"required,email"` FirstName string `json:"first_name" binding:"required"` LastName string `json:"last_name" binding:"required"` Password string `json:"password" binding:"required"` } -type UserExistDTO struct { +type UserExistRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } // }}} -// {{{ VO View Object (from service to controller) +// {{{ Response View Object (from service to controller) -type UserBaseVO struct { +type UserBaseResponse struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } -type UserInfoVO struct { - // UserBaseVO +type UserInfoResponse struct { + // UserBaseResponse ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` @@ -59,9 +59,9 @@ type UserInfoVO struct { } // }}} -// {{{ PO Persistant Object (Between the service and the repository) +// {{{ Entity Persistant Object (Between the service and the repository) -type UserPO struct { +type UserEntity struct { ID int Email string diff --git a/internal/howmuch/usecase/repo/event.go b/internal/howmuch/usecase/repo/event.go index 5b34af5..591edf0 100644 --- a/internal/howmuch/usecase/repo/event.go +++ b/internal/howmuch/usecase/repo/event.go @@ -29,19 +29,19 @@ import ( ) type EventRepository interface { - Create(ctx context.Context, evPO *model.EventPO) (*model.EventPO, error) + Create(ctx context.Context, evEntity *model.EventEntity) (*model.EventEntity, error) // UpdateInfo updates the event related information (name, descriptions) UpdateInfo() Delete() // XXX: Pay attention to the foreign key relationships - GetByID() + GetByID(ctx context.Context, eventID int) (*model.EventEntity, error) ListExpensesByUserID() // related to events of a user - ListEventsByUserID() + ListEventsByUserID(ctx context.Context, userID int) ([]model.EventBaseItemEntity, error) } type ExpenseRepository interface { @@ -55,6 +55,7 @@ type ExpenseRepository interface { type ParticipationRepository interface { Create() Delete() + CheckParticipation(ctx context.Context, userID, eventID int) error } type TransactionRepository interface { diff --git a/internal/howmuch/usecase/repo/user.go b/internal/howmuch/usecase/repo/user.go index 5ca8e60..e13ca5d 100644 --- a/internal/howmuch/usecase/repo/user.go +++ b/internal/howmuch/usecase/repo/user.go @@ -29,7 +29,7 @@ import ( ) type UserRepository interface { - Create(ctx context.Context, transaction interface{}, u *model.UserPO) (*model.UserPO, error) - GetByEmail(ctx context.Context, email string) (*model.UserPO, error) - GetByID(ctx context.Context, id int) (*model.UserPO, error) + Create(ctx context.Context, transaction interface{}, u *model.UserEntity ) (*model.UserEntity , error) + GetByEmail(ctx context.Context, email string) (*model.UserEntity , error) + GetByID(ctx context.Context, id int) (*model.UserEntity , error) } diff --git a/internal/howmuch/usecase/usecase/event.go b/internal/howmuch/usecase/usecase/event.go index 3e61863..35f66fe 100644 --- a/internal/howmuch/usecase/usecase/event.go +++ b/internal/howmuch/usecase/usecase/event.go @@ -23,8 +23,11 @@ package usecase import ( + "net/http" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" "golang.org/x/net/context" ) @@ -39,6 +42,12 @@ type eventUsecase struct { dbRepo repo.DBRepository } +var ErrNoParticipation = &errno.Errno{ + HTTP: http.StatusUnauthorized, + Code: errno.ErrorCode(errno.AuthFailureCode, "NoParticipation"), + Message: "user doesn't have access to this event", +} + // For the controller type Event interface{} @@ -46,8 +55,8 @@ func NewEventUsecase( uuc User, ev repo.EventRepository, ex repo.ExpenseRepository, - pa repo.ParticipationRepository, - tr repo.TransactionRepository, + pa repo.ParticipationRepository, // XXX: Might be handled in event + tr repo.TransactionRepository, // XXX: Might be handled in event db repo.DBRepository, ) Event { return &eventUsecase{uuc, ev, ex, pa, tr, db} @@ -55,22 +64,22 @@ func NewEventUsecase( func (evuc *eventUsecase) CreateEvent( ctx context.Context, - evDTO *model.EventCreateDTO, -) (*model.EventInfoVO, error) { - // transfer evDTO to PO + evRequest *model.EventCreateRequest, +) (*model.EventInfoResponse, error) { + // transfer evRequest to PO - evPO := &model.EventPO{ - Name: evDTO.Name, - Description: evDTO.Description, - OwnerID: evDTO.OwnerID, + evEntity := &model.EventEntity{ + Name: evRequest.Name, + Description: evRequest.Description, + OwnerID: evRequest.OwnerID, TotalAmount: 0, - DefaultCurrency: string(evDTO.DefaultCurrency), + DefaultCurrency: string(evRequest.DefaultCurrency), } data, err := evuc.dbRepo.Transaction( ctx, func(txCtx context.Context, tx interface{}) (interface{}, error) { - created, err := evuc.eventRepo.Create(ctx, evPO) + created, err := evuc.eventRepo.Create(ctx, evEntity) if err != nil { return nil, err } @@ -84,12 +93,12 @@ func (evuc *eventUsecase) CreateEvent( created.OwnerID, ) - ownerVO, err := evuc.userUC.GetUserBaseVOByID(ctx, created.OwnerID) + ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID) if err != nil { return nil, err } - evVO := &model.EventInfoVO{ + evResponse := &model.EventInfoResponse{ ID: created.ID, Name: created.Name, Description: created.Description, @@ -97,16 +106,40 @@ func (evuc *eventUsecase) CreateEvent( created.TotalAmount, model.Currency(created.DefaultCurrency), ), - Owner: ownerVO, + Owner: ownerResponse, CreatedAt: created.CreatedAt, } - return evVO, err + return evResponse, err }) if err != nil { return nil, err } - res := data.(*model.EventInfoVO) + res := data.(*model.EventInfoResponse) return res, err } + +func (evuc *eventUsecase) ListEvents( + ctx context.Context, + userID int, +) ([]model.EventBaseItemResponse, error) { + return nil, nil +} + +// GetEventDetail +func (evuc *eventUsecase) GetEventDetail( + ctx context.Context, + userID, eventID int, +) (*model.EventInfoResponse, error) { + // Check if the user has the right to get this event + err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID) + if err != nil { + return nil, ErrNoParticipation + } + + // Get the eventDetail + // TODO: This can also be put into the cache + + return nil, nil +} diff --git a/internal/howmuch/usecase/usecase/repomock/testuserrepo.go b/internal/howmuch/usecase/usecase/repomock/testuserrepo.go index 5ab9c29..29f6163 100644 --- a/internal/howmuch/usecase/usecase/repomock/testuserrepo.go +++ b/internal/howmuch/usecase/usecase/repomock/testuserrepo.go @@ -37,8 +37,8 @@ type TestUserRepository struct{} func (tur *TestUserRepository) Create( ctx context.Context, transaction interface{}, - u *model.UserPO, -) (*model.UserPO, error) { + u *model.UserEntity, +) (*model.UserEntity, error) { user := *u user.ID = 123 @@ -53,11 +53,11 @@ func (tur *TestUserRepository) Create( func (tur *TestUserRepository) GetByEmail( ctx context.Context, email string, -) (*model.UserPO, error) { +) (*model.UserEntity, error) { hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12) switch email { case "a@b.c": - return &model.UserPO{ + return &model.UserEntity{ ID: 123, Email: "a@b.c", Password: string(hashedPwd), @@ -71,11 +71,11 @@ func (tur *TestUserRepository) GetByEmail( return nil, UserTestDummyErr } -func (tur *TestUserRepository) GetByID(ctx context.Context, id int) (*model.UserPO, error) { +func (tur *TestUserRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) { hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12) switch id { case 123: - return &model.UserPO{ + return &model.UserEntity{ ID: 123, Email: "a@b.c", Password: string(hashedPwd), diff --git a/internal/howmuch/usecase/usecase/user.go b/internal/howmuch/usecase/usecase/user.go index 0814bd9..b95b60a 100644 --- a/internal/howmuch/usecase/usecase/user.go +++ b/internal/howmuch/usecase/usecase/user.go @@ -60,9 +60,9 @@ type userUsecase struct { } type User interface { - Create(ctx context.Context, u *model.UserCreateDTO) (*model.UserInfoVO, error) - Exist(ctx context.Context, u *model.UserExistDTO) error - GetUserBaseVOByID(ctx context.Context, userID int) (*model.UserBaseVO, error) + Create(ctx context.Context, u *model.UserCreateRequest) (*model.UserInfoResponse, error) + Exist(ctx context.Context, u *model.UserExistRequest) error + GetUserBaseResponseByID(ctx context.Context, userID int) (*model.UserBaseResponse, error) } func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User { @@ -74,8 +74,8 @@ func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User { func (uuc *userUsecase) Create( ctx context.Context, - u *model.UserCreateDTO, -) (*model.UserInfoVO, error) { + u *model.UserCreateRequest, +) (*model.UserInfoResponse, error) { // Hash the password encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12) if err != nil { @@ -86,7 +86,7 @@ func (uuc *userUsecase) Create( data, err := uuc.dbRepo.Transaction( ctx, func(txCtx context.Context, tx interface{}) (interface{}, error) { - created, err := uuc.userRepo.Create(txCtx, tx, &model.UserPO{ + created, err := uuc.userRepo.Create(txCtx, tx, &model.UserEntity{ Email: u.Email, Password: u.Password, FirstName: u.FirstName, @@ -117,21 +117,21 @@ func (uuc *userUsecase) Create( return nil, err } - userPO := data.(*model.UserPO) + userEntity := data.(*model.UserEntity) - user := &model.UserInfoVO{ - ID: userPO.ID, - Email: userPO.Email, - FirstName: userPO.FirstName, - LastName: userPO.LastName, - CreatedAt: userPO.CreatedAt, - UpdatedAt: userPO.UpdatedAt, + user := &model.UserInfoResponse{ + ID: userEntity.ID, + Email: userEntity.Email, + FirstName: userEntity.FirstName, + LastName: userEntity.LastName, + CreatedAt: userEntity.CreatedAt, + UpdatedAt: userEntity.UpdatedAt, } return user, nil } -func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error { +func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error { got, err := uuc.userRepo.GetByEmail(ctx, u.Email) // Any query error? if err != nil { @@ -152,15 +152,19 @@ func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error return nil } -func (uuc *userUsecase) GetUserBaseVOByID( +func (uuc *userUsecase) GetUserBaseResponseByID( ctx context.Context, userID int, -) (*model.UserBaseVO, error) { +) (*model.UserBaseResponse, error) { + // TODO: should try first to get from the cache + // If not exists, get from the DB. And then put back + // into the cache with a timeout. + // Refresh the cache when the user data is updated (for now it cannot be updated) got, err := uuc.userRepo.GetByID(ctx, userID) if err != nil { return nil, err } - userBaseVo := &model.UserBaseVO{ + userBaseVo := &model.UserBaseResponse{ ID: got.ID, FirstName: got.FirstName, LastName: got.LastName, diff --git a/internal/howmuch/usecase/usecase/user_test.go b/internal/howmuch/usecase/usecase/user_test.go index 99506ac..da85256 100644 --- a/internal/howmuch/usecase/usecase/user_test.go +++ b/internal/howmuch/usecase/usecase/user_test.go @@ -35,13 +35,13 @@ func TestCreateUser(t *testing.T) { t.Run("normal create", func(t *testing.T) { ctx := context.Background() userUsecase := NewUserUsecase(&repomock.TestUserRepository{}, &repomock.TestDBRepository{}) - input := &model.UserCreateDTO{ + input := &model.UserCreateRequest{ Email: "a@b.c", FirstName: "James", LastName: "Bond", Password: "verystrong", } - want := &model.UserInfoVO{ + want := &model.UserInfoResponse{ ID: 123, } @@ -53,7 +53,7 @@ func TestCreateUser(t *testing.T) { t.Run("duplicate create", func(t *testing.T) { ctx := context.Background() userUsecase := NewUserUsecase(&repomock.TestUserRepository{}, &repomock.TestDBRepository{}) - input := &model.UserCreateDTO{ + input := &model.UserCreateRequest{ Email: "duplicate@error.com", FirstName: "James", LastName: "Bond", @@ -68,22 +68,22 @@ func TestCreateUser(t *testing.T) { func TestUserExist(t *testing.T) { testCases := []struct { Name string - User *model.UserExistDTO + User *model.UserExistRequest ExpErr error }{ - {"user exists", &model.UserExistDTO{ + {"user exists", &model.UserExistRequest{ Email: "a@b.c", Password: "strongHashed", }, nil}, - {"query error", &model.UserExistDTO{ + {"query error", &model.UserExistRequest{ Email: "query@error.com", Password: "strongHashed", }, repomock.UserTestDummyErr}, - {"user doesn not exist", &model.UserExistDTO{ + {"user doesn not exist", &model.UserExistRequest{ Email: "inexist@error.com", Password: "strongHashed", }, UserNotExist}, - {"wrong password", &model.UserExistDTO{ + {"wrong password", &model.UserExistRequest{ Email: "a@b.c", Password: "wrongHashed", }, UserWrongPassword},