feat: Impl event while refactoring user domain
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build and test / Build (push) Successful in 2m21s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build and test / Build (push) Successful in 2m21s
				
			This commit is contained in:
		
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								README.md
									
									
									
									
									
								
							@ -376,3 +376,36 @@ functionalities well decoupled and interfaces well defined.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
I will add some tests for existing code and then it's time to move on to
 | 
					I will add some tests for existing code and then it's time to move on to
 | 
				
			||||||
my core business logic.
 | 
					my core business logic.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2024/10/16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I am facing a design problem. My way to implement the business logic is to
 | 
				
			||||||
 | 
					first write the core logic code in the domain service level. It will help me
 | 
				
			||||||
 | 
					to identify if there are any missing part in my model design. Thus, when
 | 
				
			||||||
 | 
					some of the business logic is done, I can create database migrations and then
 | 
				
			||||||
 | 
					implement the adapter level's code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The problem is that my design depends heavily on the database. Taking the
 | 
				
			||||||
 | 
					example of adding an expense to en event.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Input is a valid `ExpenseDTO` which has the `event`, `paiements` and
 | 
				
			||||||
 | 
					`receptions`. What I must do is to open a database transaction where I:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Get the Event. (Most importantly the `TotalAmount`)
 | 
				
			||||||
 | 
					2. For each `paiemnt` and `reception` create a transaction related to the
 | 
				
			||||||
 | 
					`User`. And insert them into the database.
 | 
				
			||||||
 | 
					3. Update the `TotalAmount`
 | 
				
			||||||
 | 
					4. Update the caches if any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If any step fails, the transaction rolls back.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This has barely no logic at all. I think it is not suitable to try to tie
 | 
				
			||||||
 | 
					this operation to the domain model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					However, there is something that worth a domain model level method, that
 | 
				
			||||||
 | 
					is to calculate the share of each members of the event, where we will have
 | 
				
			||||||
 | 
					the list of members and the amount of balance they have. And then we will
 | 
				
			||||||
 | 
					do the calculate and send back a list of money one should pay for another.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Finally, I think the business logic is still too simple to be put into a
 | 
				
			||||||
 | 
					"Domain". For now, the service layer is just enough.
 | 
				
			||||||
 | 
				
			|||||||
@ -35,7 +35,10 @@ func NewtestUserUsecase() usecase.User {
 | 
				
			|||||||
	return &testUserUsecase{}
 | 
						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
 | 
						return nil, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,3 +56,11 @@ func (*testUserUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error
 | 
				
			|||||||
	// Should never reach here
 | 
						// Should never reach here
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (*testUserUsecase) GetUserBaseVOByID(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						userID int,
 | 
				
			||||||
 | 
					) (*model.UserBaseVO, error) {
 | 
				
			||||||
 | 
						// TODO:
 | 
				
			||||||
 | 
						return nil, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -30,3 +30,8 @@ RETURNING *;
 | 
				
			|||||||
SELECT id, email, first_name, last_name, password, created_at, updated_at
 | 
					SELECT id, email, first_name, last_name, password, created_at, updated_at
 | 
				
			||||||
    FROM "user"
 | 
					    FROM "user"
 | 
				
			||||||
    WHERE email = $1;
 | 
					    WHERE email = $1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- name: GetUserByID :one
 | 
				
			||||||
 | 
					SELECT id, email, first_name, last_name, password, created_at, updated_at
 | 
				
			||||||
 | 
					    FROM "user"
 | 
				
			||||||
 | 
					    WHERE id = $1;
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,27 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
 | 
				
			|||||||
	return i, err
 | 
						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
 | 
					const insertUser = `-- name: InsertUser :one
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INSERT INTO "user" (
 | 
					INSERT INTO "user" (
 | 
				
			||||||
 | 
				
			|||||||
@ -108,3 +108,24 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.
 | 
				
			|||||||
		UpdatedAt: userDB.CreatedAt,
 | 
							UpdatedAt: userDB.CreatedAt,
 | 
				
			||||||
	}, nil
 | 
						}, 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -24,12 +24,37 @@ package model
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import "time"
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// {{{ DTO Data Transfer Object (from controller to service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type EventCreateDTO struct {
 | 
					type EventCreateDTO struct {
 | 
				
			||||||
	Name            string   `json:"name"        binding:"requiered"`
 | 
						Name            string   `json:"name"        binding:"requiered"`
 | 
				
			||||||
	Description     string   `json:"description"`
 | 
						Description     string   `json:"description"`
 | 
				
			||||||
	OwnerID         int      `json:"owner_id"    binding:"requiered,number"`
 | 
						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 {
 | 
					type EventPO struct {
 | 
				
			||||||
	ID int
 | 
						ID int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,21 +68,26 @@ type EventPO struct {
 | 
				
			|||||||
	UpdatedAt time.Time
 | 
						UpdatedAt time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Event struct {
 | 
					// }}}
 | 
				
			||||||
 | 
					// {{{ DO Domain Object (Contains the domain service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type EventDO struct {
 | 
				
			||||||
	ID int
 | 
						ID int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Name        string
 | 
						Name        string
 | 
				
			||||||
	Description string
 | 
						Description string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// lazy get using participation join
 | 
						// lazy get using participation join
 | 
				
			||||||
	Users []*User
 | 
						Users []*UserDO
 | 
				
			||||||
	// lazy get
 | 
						// lazy get
 | 
				
			||||||
	Expenses []*Expense
 | 
						Expenses []*Expense
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	TotalAmount     Money
 | 
						TotalAmount     Money
 | 
				
			||||||
	DefaultCurrency Currency
 | 
						DefaultCurrency Currency
 | 
				
			||||||
	Owner           User
 | 
						Owner           *UserDO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	CreatedAt time.Time
 | 
						CreatedAt time.Time
 | 
				
			||||||
	UpdatedAt time.Time
 | 
						UpdatedAt time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// }}}
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,8 @@ package model
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import "time"
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// {{{ DTO Data Transfer Object (from controller to service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserCreateDTO struct {
 | 
					type UserCreateDTO struct {
 | 
				
			||||||
	Email     string `json:"email"      binding:"required,email"`
 | 
						Email     string `json:"email"      binding:"required,email"`
 | 
				
			||||||
	FirstName string `json:"first_name" binding:"required"`
 | 
						FirstName string `json:"first_name" binding:"required"`
 | 
				
			||||||
@ -36,6 +38,29 @@ type UserExistDTO struct {
 | 
				
			|||||||
	Password string `json:"password" binding:"required"`
 | 
						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 {
 | 
					type UserPO struct {
 | 
				
			||||||
	ID int
 | 
						ID int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -48,8 +73,11 @@ type UserPO struct {
 | 
				
			|||||||
	UpdatedAt time.Time
 | 
						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
 | 
						ID int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Email     string
 | 
						Email     string
 | 
				
			||||||
@ -63,3 +91,5 @@ type User struct {
 | 
				
			|||||||
	CreatedAt time.Time
 | 
						CreatedAt time.Time
 | 
				
			||||||
	UpdatedAt time.Time
 | 
						UpdatedAt time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// }}}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										64
									
								
								internal/howmuch/usecase/repo/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								internal/howmuch/usecase/repo/event.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -31,4 +31,5 @@ import (
 | 
				
			|||||||
type UserRepository interface {
 | 
					type UserRepository interface {
 | 
				
			||||||
	Create(ctx context.Context, transaction interface{}, u *model.UserPO) (*model.UserPO, error)
 | 
						Create(ctx context.Context, transaction interface{}, u *model.UserPO) (*model.UserPO, error)
 | 
				
			||||||
	GetByEmail(ctx context.Context, email string) (*model.UserPO, error)
 | 
						GetByEmail(ctx context.Context, email string) (*model.UserPO, error)
 | 
				
			||||||
 | 
						GetByID(ctx context.Context, id int) (*model.UserPO, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										112
									
								
								internal/howmuch/usecase/usecase/event.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								internal/howmuch/usecase/usecase/event.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -50,7 +50,10 @@ func (tur *TestUserRepository) Create(
 | 
				
			|||||||
	return &user, nil
 | 
						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)
 | 
						hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
 | 
				
			||||||
	switch email {
 | 
						switch email {
 | 
				
			||||||
	case "a@b.c":
 | 
						case "a@b.c":
 | 
				
			||||||
@ -67,3 +70,20 @@ func (ur *TestUserRepository) GetByEmail(ctx context.Context, email string) (*mo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return nil, UserTestDummyErr
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -60,8 +60,9 @@ type userUsecase struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type User interface {
 | 
					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
 | 
						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 {
 | 
					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
 | 
						// Hash the password
 | 
				
			||||||
	encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12)
 | 
						encrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -100,9 +104,9 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*mo
 | 
				
			|||||||
			log.InfoLog(
 | 
								log.InfoLog(
 | 
				
			||||||
				"created new user",
 | 
									"created new user",
 | 
				
			||||||
				"email",
 | 
									"email",
 | 
				
			||||||
				u.Email,
 | 
									created.Email,
 | 
				
			||||||
				"name",
 | 
									"name",
 | 
				
			||||||
				fmt.Sprintf("%s %s", u.FirstName, u.LastName),
 | 
									fmt.Sprintf("%s %s", created.FirstName, created.LastName),
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return created, err
 | 
								return created, err
 | 
				
			||||||
@ -115,15 +119,11 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.UserCreateDTO) (*mo
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	userPO := data.(*model.UserPO)
 | 
						userPO := data.(*model.UserPO)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user := &model.User{
 | 
						user := &model.UserInfoVO{
 | 
				
			||||||
		ID:        userPO.ID,
 | 
							ID:        userPO.ID,
 | 
				
			||||||
		Email:     userPO.Email,
 | 
							Email:     userPO.Email,
 | 
				
			||||||
		Password:  userPO.Password,
 | 
					 | 
				
			||||||
		FirstName: userPO.FirstName,
 | 
							FirstName: userPO.FirstName,
 | 
				
			||||||
		LastName:  userPO.LastName,
 | 
							LastName:  userPO.LastName,
 | 
				
			||||||
 | 
					 | 
				
			||||||
		EventIDs: []int{}, // Unfilled
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		CreatedAt: userPO.CreatedAt,
 | 
							CreatedAt: userPO.CreatedAt,
 | 
				
			||||||
		UpdatedAt: userPO.UpdatedAt,
 | 
							UpdatedAt: userPO.UpdatedAt,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -151,3 +151,19 @@ func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistDTO) error
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,6 @@ import (
 | 
				
			|||||||
	"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
 | 
						"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
 | 
				
			||||||
	"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase/repomock"
 | 
						"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase/repomock"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCreateUser(t *testing.T) {
 | 
					func TestCreateUser(t *testing.T) {
 | 
				
			||||||
@ -42,23 +41,13 @@ func TestCreateUser(t *testing.T) {
 | 
				
			|||||||
			LastName:  "Bond",
 | 
								LastName:  "Bond",
 | 
				
			||||||
			Password:  "verystrong",
 | 
								Password:  "verystrong",
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		want := &model.User{
 | 
							want := &model.UserInfoVO{
 | 
				
			||||||
			ID: 123,
 | 
								ID: 123,
 | 
				
			||||||
			Email:     input.Email,
 | 
					 | 
				
			||||||
			FirstName: input.FirstName,
 | 
					 | 
				
			||||||
			LastName:  input.LastName,
 | 
					 | 
				
			||||||
			// Password is hashed
 | 
					 | 
				
			||||||
			Password: "verystrong",
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		got, err := userUsecase.Create(ctx, input)
 | 
							got, err := userUsecase.Create(ctx, input)
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
		assert.Equal(t, want.ID, got.ID)
 | 
							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) {
 | 
						t.Run("duplicate create", func(t *testing.T) {
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user