feat: Impl event while refactoring user domain
All checks were successful
Build and test / Build (push) Successful in 2m21s

This commit is contained in:
Muyao CHEN
2024-10-16 23:47:06 +02:00
parent 29633e0e95
commit a7a915d825
13 changed files with 385 additions and 32 deletions

View File

@ -35,7 +35,10 @@ func NewtestUserUsecase() usecase.User {
return &testUserUsecase{}
}
func (*testUserUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*model.User, error) {
func (*testUserUsecase) Create(
ctx context.Context,
u *model.UserCreateDTO,
) (*model.UserInfoVO, error) {
return nil, nil
}
@ -53,3 +56,11 @@ func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error
// Should never reach here
return nil
}
func (*testUserUsecase) GetUserBaseVOByID(
ctx context.Context,
userID int,
) (*model.UserBaseVO, error) {
// TODO:
return nil, nil
}

View File

@ -30,3 +30,8 @@ RETURNING *;
SELECT id, email, first_name, last_name, password, created_at, updated_at
FROM "user"
WHERE email = $1;
-- name: GetUserByID :one
SELECT id, email, first_name, last_name, password, created_at, updated_at
FROM "user"
WHERE id = $1;

View File

@ -31,6 +31,27 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT id, email, first_name, last_name, password, created_at, updated_at
FROM "user"
WHERE id = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.FirstName,
&i.LastName,
&i.Password,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const insertUser = `-- name: InsertUser :one
INSERT INTO "user" (

View File

@ -108,3 +108,24 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.
UpdatedAt: userDB.CreatedAt,
}, nil
}
func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserPO, error) {
queries := sqlc.New(ur.db)
userDB, err := queries.GetUserByID(ctx, int32(id))
if errors.Is(err, pgx.ErrNoRows) {
// No query error, but user not found
return nil, nil
} else if err != nil {
return nil, err
}
return &model.UserPO{
ID: int(userDB.ID),
Email: userDB.Email,
FirstName: userDB.FirstName,
LastName: userDB.LastName,
Password: userDB.Password,
CreatedAt: userDB.CreatedAt,
UpdatedAt: userDB.CreatedAt,
}, nil
}

View File

@ -24,12 +24,37 @@ package model
import "time"
// {{{ DTO Data Transfer Object (from controller to service)
type EventCreateDTO struct {
Name string `json:"name" binding:"requiered"`
Description string `json:"description"`
OwnerID int `json:"owner_id" binding:"requiered,number"`
Name string `json:"name" binding:"requiered"`
Description string `json:"description"`
OwnerID int `json:"owner_id" binding:"requiered,number"`
DefaultCurrency Currency `json:"currency"`
}
// }}}
// {{{ VO View Object (from service to controller)
type EventInfoVO struct {
ID int
Name string
Description string
TotalAmount Money
Owner *UserBaseVO
CreatedAt time.Time
UpdatedAt time.Time
Users []*UserBaseVO
}
// }}}
// {{{ PO Persistant Object (Between the service and the repository)
type EventPO struct {
ID int
@ -43,21 +68,26 @@ type EventPO struct {
UpdatedAt time.Time
}
type Event struct {
// }}}
// {{{ DO Domain Object (Contains the domain service)
type EventDO struct {
ID int
Name string
Description string
// lazy get using participation join
Users []*User
Users []*UserDO
// lazy get
Expenses []*Expense
TotalAmount Money
DefaultCurrency Currency
Owner User
Owner *UserDO
CreatedAt time.Time
UpdatedAt time.Time
}
// }}}

View File

@ -24,6 +24,8 @@ package model
import "time"
// {{{ DTO Data Transfer Object (from controller to service)
type UserCreateDTO struct {
Email string `json:"email" binding:"required,email"`
FirstName string `json:"first_name" binding:"required"`
@ -36,6 +38,29 @@ type UserExistDTO struct {
Password string `json:"password" binding:"required"`
}
// }}}
// {{{ VO View Object (from service to controller)
type UserBaseVO struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
type UserInfoVO struct {
// UserBaseVO
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
CreatedAt time.Time
UpdatedAt time.Time
}
// }}}
// {{{ PO Persistant Object (Between the service and the repository)
type UserPO struct {
ID int
@ -48,8 +73,11 @@ type UserPO struct {
UpdatedAt time.Time
}
// User model
type User struct {
// }}}
// {{{ DO Domain Object (Contains the domain service)
// TODO: For now I don't know what to do with this model
type UserDO struct {
ID int
Email string
@ -63,3 +91,5 @@ type User struct {
CreatedAt time.Time
UpdatedAt time.Time
}
// }}}

View File

@ -0,0 +1,64 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package repo
import (
"context"
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
)
type EventRepository interface {
Create(ctx context.Context, evPO *model.EventPO) (*model.EventPO, error)
// UpdateInfo updates the event related information (name, descriptions)
UpdateInfo()
Delete() // XXX: Pay attention to the foreign key relationships
GetByID()
ListExpensesByUserID()
// related to events of a user
ListEventsByUserID()
}
type ExpenseRepository interface {
Create()
Update()
Delete() // Delete also the related transactions
GetByID()
}
type ParticipationRepository interface {
Create()
Delete()
}
type TransactionRepository interface {
Create()
// Delete() might be handled in the Expense
// Transaction is a joined entity, we don't provide diret read operation
}

View File

@ -31,4 +31,5 @@ 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)
}

View File

@ -0,0 +1,112 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package usecase
import (
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
"golang.org/x/net/context"
)
type eventUsecase struct {
userUC User
eventRepo repo.EventRepository
expenseRepo repo.ExpenseRepository
participationRepo repo.ParticipationRepository
transactionRepo repo.TransactionRepository
dbRepo repo.DBRepository
}
// For the controller
type Event interface{}
func NewEventUsecase(
uuc User,
ev repo.EventRepository,
ex repo.ExpenseRepository,
pa repo.ParticipationRepository,
tr repo.TransactionRepository,
db repo.DBRepository,
) Event {
return &eventUsecase{uuc, ev, ex, pa, tr, db}
}
func (evuc *eventUsecase) CreateEvent(
ctx context.Context,
evDTO *model.EventCreateDTO,
) (*model.EventInfoVO, error) {
// transfer evDTO to PO
evPO := &model.EventPO{
Name: evDTO.Name,
Description: evDTO.Description,
OwnerID: evDTO.OwnerID,
TotalAmount: 0,
DefaultCurrency: string(evDTO.DefaultCurrency),
}
data, err := evuc.dbRepo.Transaction(
ctx,
func(txCtx context.Context, tx interface{}) (interface{}, error) {
created, err := evuc.eventRepo.Create(ctx, evPO)
if err != nil {
return nil, err
}
// TODO: App log, maybe can be sent to some third party service.
log.InfoLog(
"created new event",
"name",
created.Name,
"owner",
created.OwnerID,
)
ownerVO, err := evuc.userUC.GetUserBaseVOByID(ctx, created.OwnerID)
if err != nil {
return nil, err
}
evVO := &model.EventInfoVO{
ID: created.ID,
Name: created.Name,
Description: created.Description,
TotalAmount: model.MakeMoney(
created.TotalAmount,
model.Currency(created.DefaultCurrency),
),
Owner: ownerVO,
CreatedAt: created.CreatedAt,
}
return evVO, err
})
if err != nil {
return nil, err
}
res := data.(*model.EventInfoVO)
return res, err
}

View File

@ -50,7 +50,10 @@ func (tur *TestUserRepository) Create(
return &user, nil
}
func (ur *TestUserRepository) GetByEmail(ctx context.Context, email string) (*model.UserPO, error) {
func (tur *TestUserRepository) GetByEmail(
ctx context.Context,
email string,
) (*model.UserPO, error) {
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
switch email {
case "a@b.c":
@ -67,3 +70,20 @@ func (ur *TestUserRepository) GetByEmail(ctx context.Context, email string) (*mo
return nil, UserTestDummyErr
}
func (tur *TestUserRepository) GetByID(ctx context.Context, id int) (*model.UserPO, error) {
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
switch id {
case 123:
return &model.UserPO{
ID: 123,
Email: "a@b.c",
Password: string(hashedPwd),
}, nil
case 456:
return nil, UserTestDummyErr
case 789:
return nil, nil
}
return nil, UserTestDummyErr
}

View File

@ -60,8 +60,9 @@ type userUsecase struct {
}
type User interface {
Create(ctx context.Context, u *model.UserCreateDTO) (*model.User, error)
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)
}
func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User {
@ -71,7 +72,10 @@ func NewUserUsecase(r repo.UserRepository, d repo.DBRepository) User {
}
}
func (uuc *userUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*model.User, error) {
func (uuc *userUsecase) Create(
ctx context.Context,
u *model.UserCreateDTO,
) (*model.UserInfoVO, error) {
// Hash the password
encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12)
if err != nil {
@ -100,9 +104,9 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*mo
log.InfoLog(
"created new user",
"email",
u.Email,
created.Email,
"name",
fmt.Sprintf("%s %s", u.FirstName, u.LastName),
fmt.Sprintf("%s %s", created.FirstName, created.LastName),
)
return created, err
@ -115,15 +119,11 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*mo
userPO := data.(*model.UserPO)
user := &model.User{
user := &model.UserInfoVO{
ID: userPO.ID,
Email: userPO.Email,
Password: userPO.Password,
FirstName: userPO.FirstName,
LastName: userPO.LastName,
EventIDs: []int{}, // Unfilled
CreatedAt: userPO.CreatedAt,
UpdatedAt: userPO.UpdatedAt,
}
@ -151,3 +151,19 @@ func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error
return nil
}
func (uuc *userUsecase) GetUserBaseVOByID(
ctx context.Context,
userID int,
) (*model.UserBaseVO, error) {
got, err := uuc.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
userBaseVo := &model.UserBaseVO{
ID: got.ID,
FirstName: got.FirstName,
LastName: got.LastName,
}
return userBaseVo, nil
}

View File

@ -29,7 +29,6 @@ import (
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase/repomock"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/bcrypt"
)
func TestCreateUser(t *testing.T) {
@ -42,23 +41,13 @@ func TestCreateUser(t *testing.T) {
LastName: "Bond",
Password: "verystrong",
}
want := &model.User{
ID: 123,
Email: input.Email,
FirstName: input.FirstName,
LastName: input.LastName,
// Password is hashed
Password: "verystrong",
want := &model.UserInfoVO{
ID: 123,
}
got, err := userUsecase.Create(ctx, input)
assert.NoError(t, err)
assert.Equal(t, want.ID, got.ID)
assert.NoError(
t,
bcrypt.CompareHashAndPassword([]byte(got.Password), []byte(want.Password)),
)
})
t.Run("duplicate create", func(t *testing.T) {