diff --git a/Makefile b/Makefile index d8e12fc..ed9b311 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,8 @@ mock-gen: mockgen -source ./internal/pin/pin.utils.go -destination ./mocks/pin/pin.utils.go mockgen -source ./internal/stamp/stamp.repository.go -destination ./mocks/stamp/stamp.repository.go mockgen -source ./internal/stamp/stamp.service.go -destination ./mocks/stamp/stamp.service.go + mockgen -source ./internal/selection/selection.repository.go -destination ./mocks/selection/selection.repository.go + mockgen -source ./internal/selection/selection.service.go -destination ./mocks/selection/selection.service.go test: go vet ./... diff --git a/cmd/main.go b/cmd/main.go index cf840dc..1d99558 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,10 +13,15 @@ import ( "github.com/isd-sgcu/rpkm67-backend/config" "github.com/isd-sgcu/rpkm67-backend/constant" "github.com/isd-sgcu/rpkm67-backend/database" + "github.com/isd-sgcu/rpkm67-backend/internal/cache" + "github.com/isd-sgcu/rpkm67-backend/internal/group" "github.com/isd-sgcu/rpkm67-backend/internal/pin" + "github.com/isd-sgcu/rpkm67-backend/internal/selection" "github.com/isd-sgcu/rpkm67-backend/internal/stamp" "github.com/isd-sgcu/rpkm67-backend/logger" + groupProto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/group/v1" pinProto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/pin/v1" + selectionProto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/selection/v1" stampProto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/stamp/v1" "go.uber.org/zap" "google.golang.org/grpc" @@ -43,7 +48,7 @@ func main() { panic(fmt.Sprintf("Failed to connect to redis: %v", err)) } - // cacheRepo := cache.NewRepository(redis) + cacheRepo := cache.NewRepository(redis) pinRepo := pin.NewRepository(redis) pinUtils := pin.NewUtils() @@ -52,7 +57,11 @@ func main() { stampRepo := stamp.NewRepository(db) stampSvc := stamp.NewService(stampRepo, constant.ActivityIdToIdx, logger.Named("stampSvc")) - // selectionRepo := selection.NewRepository(db) + groupRepo := group.NewRepository(db) + groupSvc := group.NewService(groupRepo, cacheRepo, logger.Named("groupSvc")) + + selectionRepo := selection.NewRepository(db) + selectionSvc := selection.NewService(selectionRepo, cacheRepo, logger.Named("selectionSvc")) listener, err := net.Listen("tcp", fmt.Sprintf(":%v", conf.App.Port)) if err != nil { @@ -63,6 +72,8 @@ func main() { grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer()) pinProto.RegisterPinServiceServer(grpcServer, pinSvc) stampProto.RegisterStampServiceServer(grpcServer, stampSvc) + groupProto.RegisterGroupServiceServer(grpcServer, groupSvc) + selectionProto.RegisterSelectionServiceServer(grpcServer, selectionSvc) reflection.Register(grpcServer) go func() { diff --git a/go.mod b/go.mod index d6cd2f4..cb668e0 100644 --- a/go.mod +++ b/go.mod @@ -4,23 +4,22 @@ go 1.22.4 require ( github.com/golang/mock v1.6.0 - github.com/isd-sgcu/rpkm67-go-proto v0.2.5 + github.com/google/uuid v1.6.0 + github.com/isd-sgcu/rpkm67-go-proto v0.2.8 github.com/isd-sgcu/rpkm67-model v0.0.6 github.com/joho/godotenv v1.5.1 github.com/redis/go-redis/v9 v9.5.3 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - google.golang.org/grpc v1.64.0 + google.golang.org/grpc v1.65.0 gorm.io/driver/postgres v1.5.9 gorm.io/gorm v1.25.10 ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -31,11 +30,12 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1812fb9..6962b41 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -15,24 +15,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/isd-sgcu/rpkm67-go-proto v0.1.6 h1:6mubghe7HuGJYv+hgpIxJBBrmVk5UdjHjdvzykLLhQ0= -github.com/isd-sgcu/rpkm67-go-proto v0.1.6/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-go-proto v0.1.7 h1:FO8B4A7iuVS8PM4cEXe3HxZxlqdDY8va1zxy7TAnkqY= -github.com/isd-sgcu/rpkm67-go-proto v0.1.7/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-go-proto v0.1.9 h1:UqCu7uucb6o6NgmGEKbc6In0hBR5HDtvn+FiJ1mipVw= -github.com/isd-sgcu/rpkm67-go-proto v0.1.9/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-go-proto v0.2.0 h1:tPfNgCuqS4g0f+2hzcpY+8hYXSa7DZDPvRejRzOk2cI= -github.com/isd-sgcu/rpkm67-go-proto v0.2.0/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-go-proto v0.2.3 h1:u4ROlwsTmzcXgW3ED2UobPvF1OF+jykHH9AEH8F3XcU= -github.com/isd-sgcu/rpkm67-go-proto v0.2.3/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-go-proto v0.2.5 h1:lNpRUnPr6QiLBnexeCM2MvlNHeUlJ/jbKdrYWj1/qtk= -github.com/isd-sgcu/rpkm67-go-proto v0.2.5/go.mod h1:Z5SYz5kEe4W+MdqPouF0zEOiaqvg+s9I1S5d0q6e+Jw= -github.com/isd-sgcu/rpkm67-model v0.0.1 h1:LFn7jaawkZP1golE9B32a2KL/U/w20UFjQo2Cd/3Fhc= -github.com/isd-sgcu/rpkm67-model v0.0.1/go.mod h1:dxgLSkrFpbQOXsrzqgepZoEOyZUIG2LBGtm5gsuBbVc= -github.com/isd-sgcu/rpkm67-model v0.0.4 h1:tk6z6pXnhWBoG2SaSIoyLxNnwRaXwdbSIEEa/cSi8EY= -github.com/isd-sgcu/rpkm67-model v0.0.4/go.mod h1:dxgLSkrFpbQOXsrzqgepZoEOyZUIG2LBGtm5gsuBbVc= -github.com/isd-sgcu/rpkm67-model v0.0.5 h1:S+uza3ps1CP+JzgGILg6WwqlFJFaBsOIsuNBiJWJug8= -github.com/isd-sgcu/rpkm67-model v0.0.5/go.mod h1:dxgLSkrFpbQOXsrzqgepZoEOyZUIG2LBGtm5gsuBbVc= +github.com/isd-sgcu/rpkm67-go-proto v0.2.8 h1:YDkxRcu204XD70E+xJSYt/4XmwXuM13nVNiEWflc73c= +github.com/isd-sgcu/rpkm67-go-proto v0.2.8/go.mod h1:w+UCeQnJ3wBuJ7Tyf8LiBiPZVb1KlecjMNCB7kBeL7M= github.com/isd-sgcu/rpkm67-model v0.0.6 h1:pYlqOmeXGQIfHdOhyAta4kXkqnoLc4X3KWcAjPrAuds= github.com/isd-sgcu/rpkm67-model v0.0.6/go.mod h1:dxgLSkrFpbQOXsrzqgepZoEOyZUIG2LBGtm5gsuBbVc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -81,8 +65,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= @@ -105,10 +89,10 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/group/group.repository.go b/internal/group/group.repository.go index de9c54a..2ddf043 100644 --- a/internal/group/group.repository.go +++ b/internal/group/group.repository.go @@ -1,10 +1,16 @@ package group import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/isd-sgcu/rpkm67-model/model" "gorm.io/gorm" ) type Repository interface { + FindOne(userId uuid.UUID) (*model.Group, error) } type repositoryImpl struct { @@ -16,3 +22,19 @@ func NewRepository(db *gorm.DB) Repository { Db: db, } } + +func (r *repositoryImpl) FindOne(userId uuid.UUID) (*model.Group, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + var group model.Group + if err := r.Db.WithContext(ctx). + Preload("Members"). + Joins("JOIN users ON users.group_id = groups.id"). + Where("users.id = ?", userId). + First(&group).Error; err != nil { + return nil, err + } + + return &group, nil +} diff --git a/internal/group/group.service.go b/internal/group/group.service.go index 8f03ce2..3097219 100644 --- a/internal/group/group.service.go +++ b/internal/group/group.service.go @@ -2,7 +2,10 @@ package group import ( "context" + "fmt" + "github.com/google/uuid" + "github.com/isd-sgcu/rpkm67-backend/internal/cache" proto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/group/v1" "go.uber.org/zap" ) @@ -13,17 +16,75 @@ type Service interface { type serviceImpl struct { proto.UnimplementedGroupServiceServer - repo Repository - log *zap.Logger + repo Repository + cache cache.Repository + log *zap.Logger } -func NewService(repo Repository, log *zap.Logger) Service { +func NewService(repo Repository, cache cache.Repository, log *zap.Logger) Service { return &serviceImpl{ - repo: repo, - log: log, + repo: repo, + cache: cache, + log: log, } } -func (s *serviceImpl) FindOne(_ context.Context, in *proto.FindOneGroupRequest) (res *proto.FindOneGroupResponse, err error) { - return nil, nil +func (s *serviceImpl) FindOne(ctx context.Context, in *proto.FindOneGroupRequest) (*proto.FindOneGroupResponse, error) { + cacheKey := fmt.Sprintf("group:%s", in.UserId) + var cachedGroup proto.Group + + // try to retreive from cache + err := s.cache.GetValue(cacheKey, &cachedGroup) + if err == nil { + s.log.Info("Group found in cache", zap.String("user_id", in.UserId)) + return &proto.FindOneGroupResponse{Group: &cachedGroup}, nil + } + + userUUID, err := uuid.Parse(in.UserId) + if err != nil { + return nil, fmt.Errorf("invalid UUID format: %v", err) + } + + // if not found cache, find group in database + group, err := s.repo.FindOne(userUUID) + if err != nil { + s.log.Error("Failed to find group", zap.String("user_id", in.UserId), zap.Error(err)) + return nil, err + } + + userInfo := make([]*proto.UserInfo, 0, len(group.Members)) + for _, m := range group.Members { + user := proto.UserInfo{ + Id: m.ID.String(), + Firstname: m.Firstname, + Lastname: m.Lastname, + ImageUrl: m.PhotoUrl, + } + userInfo = append(userInfo, &user) + } + + groupRPC := proto.Group{ + Id: group.ID.String(), + LeaderID: group.LeaderID, + Token: group.Token, + Members: userInfo, + IsConfirmed: group.IsConfirmed, + } + + // set cache + if err := s.cache.SetValue(cacheKey, &groupRPC, 3600); err != nil { // cache นาน 1 ชั่วโมง + s.log.Warn("Failed to set group in cache", zap.String("user_id", in.UserId), zap.Error(err)) + } + + res := proto.FindOneGroupResponse{ + Group: &groupRPC, + } + + s.log.Info("FindOne group service completed", + zap.String("group_id", group.ID.String()), + zap.String("user_id", in.UserId), + zap.Int("member_count", len(userInfo)), + zap.Bool("from_cache", false)) + + return &res, nil } diff --git a/internal/selection/selection.repository.go b/internal/selection/selection.repository.go index 82a58ee..adc5631 100644 --- a/internal/selection/selection.repository.go +++ b/internal/selection/selection.repository.go @@ -8,8 +8,11 @@ import ( type Repository interface { Create(user *model.Selection) error FindByGroupId(groupId string, selections *[]model.Selection) error - Delete(id string) error - CountGroupByBaanId() (map[string]int, error) + Delete(groupId string, baanId string) error + CountByBaanId() (map[string]int, error) + UpdateNewBaanExistOrder(updateSelection *model.Selection) error + UpdateExistBaanExistOrder(updateSelection *model.Selection) error + UpdateExistBaanNewOrder(updateSelection *model.Selection) error } type repositoryImpl struct { @@ -27,26 +30,83 @@ func (r *repositoryImpl) Create(user *model.Selection) error { } func (r *repositoryImpl) FindByGroupId(groupId string, selections *[]model.Selection) error { - return r.Db.Find(selections, "groupId = ?", groupId).Error + return r.Db.Find(selections, "group_id = ?", groupId).Error } -func (r *repositoryImpl) Delete(id string) error { - return r.Db.Delete(&model.Selection{}, "id = ?", id).Error +func (r *repositoryImpl) Delete(groupId string, baanId string) error { + return r.Db.Delete(&model.Selection{}, "group_id = ? AND baan = ?", groupId, baanId).Error } -func (r *repositoryImpl) CountGroupByBaanId() (map[string]int, error) { +func (r *repositoryImpl) CountByBaanId() (map[string]int, error) { var result []struct { - BaanId string - Count int + Baan string + Count int } - if err := r.Db.Model(&model.Selection{}).Select("baan_id, count(*) as count").Group("baan_id").Scan(&result).Error; err != nil { + if err := r.Db.Model(&model.Selection{}).Select("baan, count(*) as count").Group("baan").Scan(&result).Error; err != nil { return nil, err } count := make(map[string]int) for _, v := range result { - count[v.BaanId] = v.Count + count[v.Baan] = v.Count } return count, nil } + +func (r *repositoryImpl) UpdateNewBaanExistOrder(updateSelection *model.Selection) error { + return r.Db.Transaction(func(tx *gorm.DB) error { + var existingSelection model.Selection + if err := tx.Where(`group_id = ? AND "order" = ?`, updateSelection.GroupID, updateSelection.Order).First(&existingSelection).Error; err != nil { + return err + } + + if err := tx.Where(`"order" = ? AND group_id = ?`, updateSelection.Order, updateSelection.GroupID).Model(&existingSelection).Update("baan", updateSelection.Baan).Error; err != nil { + return err + } + + return nil + }) +} + +func (r *repositoryImpl) UpdateExistBaanExistOrder(updateSelection *model.Selection) error { + return r.Db.Transaction(func(tx *gorm.DB) error { + var existingBaanSelection model.Selection + if err := tx.Where("group_id = ? AND baan = ?", updateSelection.GroupID, updateSelection.Baan).First(&existingBaanSelection).Error; err != nil { + return err + } + + var existingOrderSelection model.Selection + if err := tx.Where(`group_id = ? AND "order" = ?`, updateSelection.GroupID, updateSelection.Order).First(&existingOrderSelection).Error; err != nil { + return err + } + + if existingBaanSelection.Order == updateSelection.Order { + return nil + } + + if err := tx.Where(`"order" = ? AND group_id = ?`, existingBaanSelection.Order, updateSelection.GroupID).Model(&existingBaanSelection).Update("baan", existingOrderSelection.Baan).Error; err != nil { + return err + } + if err := tx.Where(`"order" = ? AND group_id = ?`, existingOrderSelection.Order, updateSelection.GroupID).Model(&existingOrderSelection).Update("baan", updateSelection.Baan).Error; err != nil { + return err + } + + return nil + }) +} + +func (r *repositoryImpl) UpdateExistBaanNewOrder(updateSelection *model.Selection) error { + return r.Db.Transaction(func(tx *gorm.DB) error { + var existingSelection model.Selection + if err := tx.Where("group_id = ? AND baan = ?", updateSelection.GroupID, updateSelection.Baan).First(&existingSelection).Error; err != nil { + return err + } + + if err := tx.Where("baan = ? AND group_id = ?", updateSelection.Baan, updateSelection.GroupID).Model(&existingSelection).Update("order", updateSelection.Order).Error; err != nil { + return err + } + + return nil + }) +} diff --git a/internal/selection/selection.service.go b/internal/selection/selection.service.go new file mode 100644 index 0000000..325c693 --- /dev/null +++ b/internal/selection/selection.service.go @@ -0,0 +1,251 @@ +package selection + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/isd-sgcu/rpkm67-backend/internal/cache" + proto "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/selection/v1" + "github.com/isd-sgcu/rpkm67-model/model" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Service interface { + proto.SelectionServiceServer +} + +type serviceImpl struct { + proto.UnimplementedSelectionServiceServer + repo Repository + cache cache.Repository + log *zap.Logger +} + +func NewService(repo Repository, cache cache.Repository, log *zap.Logger) Service { + return &serviceImpl{ + repo: repo, + cache: cache, + log: log, + } +} + +func (s *serviceImpl) Create(ctx context.Context, in *proto.CreateSelectionRequest) (*proto.CreateSelectionResponse, error) { + groupUUID, err := uuid.Parse(in.GroupId) + if err != nil { + s.log.Named("Create").Error(fmt.Sprintf("Parse group id: %s", in.GroupId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + selections := &[]model.Selection{} + err = s.repo.FindByGroupId(in.GroupId, selections) + if err != nil { + s.log.Named("Create").Error(fmt.Sprintf("FindByGroupId: group_id=%s", in.GroupId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + //Check can not create selection with same order + for _, selection := range *selections { + if selection.Order == int(in.Order) { + s.log.Named("Create").Error(fmt.Sprintf("Failed to create selection: order=%d", in.Order), zap.Error(err)) + return nil, status.Error(codes.Internal, "Can not create selection with same order") + } + } + + //Check can not create selection with same baan + for _, selection := range *selections { + if selection.Baan == in.BaanId { + s.log.Named("Create").Error(fmt.Sprintf("Failed to create selection: baan_id=%s", in.BaanId), zap.Error(err)) + return nil, status.Error(codes.Internal, "Can not create selection with same baan") + } + } + + //Order must be in range 1-5 + if in.Order < 1 || in.Order > 5 { + s.log.Named("Create").Error(fmt.Sprintf("Failed to create selection: order=%d", in.Order), zap.Error(err)) + return nil, status.Error(codes.Internal, "Order must be in range 1-5") + } + + //Create selection + selection := model.Selection{ + GroupID: &groupUUID, + Baan: in.BaanId, + Order: int(in.Order), + } + + err = s.repo.Create(&selection) + if err != nil { + s.log.Named("Create").Error(fmt.Sprintf("Create: group_id=%s, baan_id=%s", in.GroupId, in.BaanId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + res := proto.CreateSelectionResponse{ + Selection: &proto.Selection{ + Id: "", + GroupId: in.GroupId, + BaanId: in.BaanId, + Order: in.Order, + }, + } + + s.log.Info("Selection created", + zap.String("group_id", in.GroupId), + zap.String("baan_id", in.BaanId)) + + return &res, nil +} + +func (s *serviceImpl) FindByGroupId(ctx context.Context, in *proto.FindByGroupIdSelectionRequest) (*proto.FindByGroupIdSelectionResponse, error) { + selection := &[]model.Selection{} + + err := s.repo.FindByGroupId(in.GroupId, selection) + if err != nil { + s.log.Named("FindByGroupId").Error(fmt.Sprintf("FindByGroupId: group_id=%s", in.GroupId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + selectionRPC := []*proto.Selection{} + for _, m := range *selection { + ss := &proto.Selection{ + Id: "", + GroupId: m.GroupID.String(), + BaanId: m.Baan, + Order: int32(m.Order), + } + selectionRPC = append(selectionRPC, ss) + } + + res := proto.FindByGroupIdSelectionResponse{ + Selections: selectionRPC, + } + + s.log.Info("Selection found", + zap.String("group_id", in.GroupId), + zap.Any("selections", selectionRPC)) + + return &res, nil +} + +func (s *serviceImpl) Delete(ctx context.Context, in *proto.DeleteSelectionRequest) (*proto.DeleteSelectionResponse, error) { + err := s.repo.Delete(in.GroupId, in.BaanId) + if err != nil { + s.log.Named("Delete").Error(fmt.Sprintf("Delete: group_id=%s, baan_id=%s", in.GroupId, in.BaanId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + s.log.Info("Selection deleted", + zap.String("group_id", in.GroupId)) + + return &proto.DeleteSelectionResponse{Success: true}, nil +} + +func (s *serviceImpl) CountByBaanId(ctx context.Context, in *proto.CountByBaanIdSelectionRequest) (*proto.CountByBaanIdSelectionResponse, error) { + cachedKey := "countByBaanId" + var cachedCount *proto.CountByBaanIdSelectionResponse + + err := s.cache.GetValue(cachedKey, &cachedCount) + if err == nil { + s.log.Named("CountByBaanId").Info("Count group by baan id found in cache") + return &proto.CountByBaanIdSelectionResponse{ + BaanCounts: cachedCount.BaanCounts, + }, nil + } + + count, err := s.repo.CountByBaanId() + if err != nil { + s.log.Named("CountByBaanId").Error("CountByBaanId", zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + countRPC := []*proto.BaanCount{} + for k, v := range count { + bc := &proto.BaanCount{ + BaanId: k, + Count: int32(v), + } + countRPC = append(countRPC, bc) + } + + res := proto.CountByBaanIdSelectionResponse{ + BaanCounts: countRPC, + } + + if err := s.cache.SetValue(cachedKey, &res, 3600); err != nil { + s.log.Named("CountByBaanId").Warn("Failed to set count group by baan id in cache", zap.Error(err)) + } + + s.log.Info("Count group by baan id", + zap.Any("count", count)) + + return &res, nil +} + +func (s *serviceImpl) Update(ctx context.Context, in *proto.UpdateSelectionRequest) (*proto.UpdateSelectionResponse, error) { + oldSelections := &[]model.Selection{} + + err := s.repo.FindByGroupId(in.Selection.GroupId, oldSelections) + if err != nil { + s.log.Named("Update").Error(fmt.Sprintf("FindByGroupId: group_id=%s", in.Selection.GroupId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + groupUUID, err := uuid.Parse(in.Selection.GroupId) + if err != nil { + s.log.Named("Update").Error(fmt.Sprintf("Parse group id: %s", in.Selection.GroupId), zap.Error(err)) + return nil, status.Error(codes.Internal, err.Error()) + } + + //Order must be in range 1-5 + if in.Selection.Order < 1 || in.Selection.Order > 5 { + s.log.Named("Update").Error(fmt.Sprintf("Failed to update selection: order=%d", in.Selection.Order), zap.Error(err)) + return nil, status.Error(codes.Internal, "Order must be in range 1-5") + } + + newSelection := model.Selection{ + GroupID: &groupUUID, + Baan: in.Selection.BaanId, + Order: int(in.Selection.Order), + } + + // Check if the new Baan exists in oldSelections + baanExists := false + orderExists := false + for _, oldSel := range *oldSelections { + if oldSel.Baan == newSelection.Baan { + baanExists = true + } + if oldSel.Order == newSelection.Order { + orderExists = true + } + } + + var updateErr error + + if !baanExists && orderExists { + updateErr = s.repo.UpdateNewBaanExistOrder(&newSelection) + } else if baanExists && orderExists { + updateErr = s.repo.UpdateExistBaanExistOrder(&newSelection) + } else if baanExists && !orderExists { + updateErr = s.repo.UpdateExistBaanNewOrder(&newSelection) + } else { + s.log.Named("Update").Error(fmt.Sprintf("Invalid update scenario: group_id=%s, baan_id=%s", in.Selection.GroupId, in.Selection.BaanId)) + return nil, status.Error(codes.Internal, "Invalid update scenario") + } + + if updateErr != nil { + s.log.Named("Update").Error(fmt.Sprintf("Update: group_id=%s, baan_id=%s", in.Selection.GroupId, in.Selection.BaanId), zap.Error(updateErr)) + return nil, status.Error(codes.Internal, updateErr.Error()) + } + + res := proto.UpdateSelectionResponse{ + Success: true, + } + + s.log.Info("Selection updated", + zap.String("group_id", in.Selection.GroupId), + zap.String("baan_id", in.Selection.BaanId)) + + return &res, nil +} diff --git a/internal/selection/test/selection.service_test.go b/internal/selection/test/selection.service_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/internal/selection/test/selection.service_test.go @@ -0,0 +1 @@ +package test diff --git a/mocks/selection/selection.repository.go b/mocks/selection/selection.repository.go new file mode 100644 index 0000000..1fd7268 --- /dev/null +++ b/mocks/selection/selection.repository.go @@ -0,0 +1,92 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/selection/selection.repository.go + +// Package mock_selection is a generated GoMock package. +package mock_selection + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/isd-sgcu/rpkm67-model/model" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// CountByBaanId mocks base method. +func (m *MockRepository) CountByBaanId() (map[string]int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountByBaanId") + ret0, _ := ret[0].(map[string]int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountByBaanId indicates an expected call of CountByBaanId. +func (mr *MockRepositoryMockRecorder) CountByBaanId() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountByBaanId", reflect.TypeOf((*MockRepository)(nil).CountByBaanId)) +} + +// Create mocks base method. +func (m *MockRepository) Create(user *model.Selection) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", user) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockRepositoryMockRecorder) Create(user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), user) +} + +// Delete mocks base method. +func (m *MockRepository) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRepositoryMockRecorder) Delete(id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id) +} + +// FindByGroupId mocks base method. +func (m *MockRepository) FindByGroupId(groupId string, selections *[]model.Selection) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByGroupId", groupId, selections) + ret0, _ := ret[0].(error) + return ret0 +} + +// FindByGroupId indicates an expected call of FindByGroupId. +func (mr *MockRepositoryMockRecorder) FindByGroupId(groupId, selections interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByGroupId", reflect.TypeOf((*MockRepository)(nil).FindByGroupId), groupId, selections) +} diff --git a/mocks/selection/selection.service.go b/mocks/selection/selection.service.go new file mode 100644 index 0000000..05419a9 --- /dev/null +++ b/mocks/selection/selection.service.go @@ -0,0 +1,108 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/selection/selection.service.go + +// Package mock_selection is a generated GoMock package. +package mock_selection + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1 "github.com/isd-sgcu/rpkm67-go-proto/rpkm67/backend/selection/v1" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// CountByBaanId mocks base method. +func (m *MockService) CountByBaanId(arg0 context.Context, arg1 *v1.CountByBaanIdSelectionRequest) (*v1.CountByBaanIdSelectionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CountByBaanId", arg0, arg1) + ret0, _ := ret[0].(*v1.CountByBaanIdSelectionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountByBaanId indicates an expected call of CountByBaanId. +func (mr *MockServiceMockRecorder) CountByBaanId(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountByBaanId", reflect.TypeOf((*MockService)(nil).CountByBaanId), arg0, arg1) +} + +// Create mocks base method. +func (m *MockService) Create(arg0 context.Context, arg1 *v1.CreateSelectionRequest) (*v1.CreateSelectionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*v1.CreateSelectionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockServiceMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockService)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockService) Delete(arg0 context.Context, arg1 *v1.DeleteSelectionRequest) (*v1.DeleteSelectionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(*v1.DeleteSelectionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockServiceMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockService)(nil).Delete), arg0, arg1) +} + +// FindByGroupId mocks base method. +func (m *MockService) FindByGroupId(arg0 context.Context, arg1 *v1.FindByGroupIdSelectionRequest) (*v1.FindByGroupIdSelectionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByGroupId", arg0, arg1) + ret0, _ := ret[0].(*v1.FindByGroupIdSelectionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByGroupId indicates an expected call of FindByGroupId. +func (mr *MockServiceMockRecorder) FindByGroupId(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByGroupId", reflect.TypeOf((*MockService)(nil).FindByGroupId), arg0, arg1) +} + +// mustEmbedUnimplementedSelectionServiceServer mocks base method. +func (m *MockService) mustEmbedUnimplementedSelectionServiceServer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedSelectionServiceServer") +} + +// mustEmbedUnimplementedSelectionServiceServer indicates an expected call of mustEmbedUnimplementedSelectionServiceServer. +func (mr *MockServiceMockRecorder) mustEmbedUnimplementedSelectionServiceServer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedSelectionServiceServer", reflect.TypeOf((*MockService)(nil).mustEmbedUnimplementedSelectionServiceServer)) +}