diff --git a/Makefile b/Makefile index f6c7fde..bb412ce 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,30 @@ codegen: @echo "Generating code..." @oapi-codegen --config=./oapi-codegen.cli.yaml ./api/v1/openapi.yaml + +POSTGRES_URL="pgx5://yggdrasil_user:yggdrasil@localhost:5432/yggdrasil?sslmode=disable" + +.PHONY: create-migration +create-migration: + migrate create -ext sql -dir ./migrations -seq -digits 3 ${name} + +.PHONY: local-migrate-up +local-migrate-up: + migrate -database ${POSTGRES_URL} -path ./migrations up + +.PHONY: local-migrate-force +local-migrate-force: + migrate -database ${POSTGRES_URL} -path ./migrations force + +.PHONY: local-migrate-down +local-migrate-down: + migrate -database ${POSTGRES_URL} -path ./migrations down + +.PHONY: local-migrate-drop +local-migrate-drop: + migrate -database ${POSTGRES_URL} -path ./migrations drop -f + +.PHONY: local-db-seed +local-db-seed: + @echo "Seeding database..." + @APP_ENV="development" DATABASE_URL_FILE="postgres_url" go run ./cmd/seed/main.go diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..74cb272 --- /dev/null +++ b/cmd/seed/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + pgxDeciaml "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + pgxUUID "github.com/vgarvardt/pgx-google-uuid/v5" + "gitlab.mareshq.com/hq/yggdrasil/internal/bootstrap" + "gitlab.mareshq.com/hq/yggdrasil/internal/faker" + "gitlab.mareshq.com/hq/yggdrasil/pkg/training" + "go.uber.org/zap" + "os" + "os/signal" + "strings" + "syscall" +) + +func main() { + shutdownCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + logger := bootstrap.Logger() + + databaseURLFile := os.Getenv("DATABASE_URL_FILE") + if databaseURLFile == "" { + logger.Fatal("DATABASE_URL_FILE is not set") + } + + dat, err := os.ReadFile(databaseURLFile) + if err != nil { + logger.Fatal("Error reading DATABASE_URL_FILE", zap.Error(err)) + } + + databaseURL := string(dat) + + // clean up url from invalid characters + databaseURL = strings.ReplaceAll(databaseURL, "\n", "") + databaseURL = strings.ReplaceAll(databaseURL, "\t", "") + + pgxConfig, err := pgxpool.ParseConfig(databaseURL) + if err != nil { + logger.Fatal("Error parsing database url with pgx (database driver)", zap.Error(err)) + } + + pgxConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxUUID.Register(conn.TypeMap()) + pgxDeciaml.Register(conn.TypeMap()) + return nil + } + + pool, err := pgxpool.NewWithConfig(shutdownCtx, pgxConfig) + if err != nil { + logger.Fatal("Error creating pgx pool", zap.Error(err)) + } + + trainingRepo := training.NewPostgresTrainingRepository(pool) + trainingDateRepo := training.NewPostgresTrainingDateRepository(pool) + //trainingDateAttendeeRepo := training.NewPostgresTrainingDateAttendeeRepository(pool) + + f := faker.NewFaker(trainingRepo, trainingDateRepo) + if err := f.GenerateFakeData(); err != nil { + logger.Fatal("Error generating fake data", zap.Error(err)) + } + + logger.Info("Fake data generated successfully!") +} diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go index 0b349ff..56b3627 100644 --- a/cmd/yggdrasil/main.go +++ b/cmd/yggdrasil/main.go @@ -2,36 +2,18 @@ package main import ( "context" + "gitlab.mareshq.com/hq/yggdrasil/internal/bootstrap" "os/signal" "syscall" - - "gitlab.mareshq.com/hq/yggdrasil/internal/faker" - "gitlab.mareshq.com/hq/yggdrasil/internal/server" - "gitlab.mareshq.com/hq/yggdrasil/pkg/training" - "go.uber.org/zap" ) //go:generate go run github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen --config=../../oapi-codegen.yaml ../../api/v1/openapi.yaml -var port = 3000 - func main() { shutdownCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() - logger := zap.Must(zap.NewDevelopment()) - defer logger.Sync() - - trainingRepository := training.NewInMemoryTrainingRepository() - trainingDateRepository := training.NewInMemoryTrainingDateRepository() - trainingDateAttendeeRepository := training.NewInMemoryTrainingDateAttendeeRepository() - - f := faker.NewFaker(trainingRepository, trainingDateRepository) - if err := f.GenerateFakeData(); err != nil { - logger.Fatal("Error generating fake data", zap.Error(err)) - } - - apiHandlers := server.NewAPIHandlers(trainingRepository, trainingDateRepository, trainingDateAttendeeRepository) - srv := server.NewServer(apiHandlers, port, logger) + logger := bootstrap.Logger() + srv := bootstrap.Bootstrap(logger) srv.Run(shutdownCtx) } diff --git a/go.mod b/go.mod index 73876ac..f52c1d0 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,14 @@ require ( github.com/gofiber/contrib/swagger v1.1.2 github.com/gofiber/fiber/v2 v2.52.4 github.com/google/uuid v1.6.0 + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e + github.com/jackc/pgx/v5 v5.5.5 github.com/oapi-codegen/fiber-middleware v1.0.2 github.com/oapi-codegen/runtime v1.1.1 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.9.0 + github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0 go.uber.org/zap v1.27.0 ) @@ -33,6 +37,9 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/invopop/yaml v0.3.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -50,7 +57,10 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 874a2f8..882009b 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,18 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -123,6 +135,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -137,6 +150,8 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0 h1:kIIQmW04MYKyRE2ZwREPl1NY4/Uxf5x48ABTQ+yFdFo= +github.com/vgarvardt/pgx-google-uuid/v5 v5.0.0/go.mod h1:fskJeXpJTJCU9JvsZQRgR4OhKKpciztvx4rdXWil7E0= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= @@ -156,6 +171,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -164,6 +181,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -182,6 +201,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go new file mode 100644 index 0000000..0d3332d --- /dev/null +++ b/internal/bootstrap/bootstrap.go @@ -0,0 +1,95 @@ +package bootstrap + +import ( + "context" + pgxDeciaml "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + pgxUUID "github.com/vgarvardt/pgx-google-uuid/v5" + "gitlab.mareshq.com/hq/yggdrasil/internal/faker" + "gitlab.mareshq.com/hq/yggdrasil/internal/server" + "gitlab.mareshq.com/hq/yggdrasil/pkg/training" + "go.uber.org/zap" + "os" + "strconv" + "strings" +) + +func Bootstrap(logger *zap.Logger) *server.Server { + portStr := os.Getenv("APP_PORT") + if portStr == "" { + logger.Fatal("APP_PORT is not set") + } + + port, err := strconv.Atoi(portStr) + if err != nil { + logger.Fatal("Error parsing APP_PORT", zap.Error(err)) + } + + databaseURLFile := os.Getenv("DATABASE_URL_FILE") + if databaseURLFile == "" { + logger.Fatal("DATABASE_URL_FILE is not set") + } + + dat, err := os.ReadFile(databaseURLFile) + if err != nil { + logger.Fatal("Error reading DATABASE_URL_FILE", zap.Error(err)) + } + + databaseURL := string(dat) + + // clean up url from invalid characters + databaseURL = strings.ReplaceAll(databaseURL, "\n", "") + databaseURL = strings.ReplaceAll(databaseURL, "\t", "") + + pgxConfig, err := pgxpool.ParseConfig(databaseURL) + if err != nil { + logger.Fatal("Error parsing database url with pgx (database driver)", zap.Error(err)) + } + + pgxConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { + pgxUUID.Register(conn.TypeMap()) + pgxDeciaml.Register(conn.TypeMap()) + return nil + } + + pool, err := pgxpool.NewWithConfig(context.TODO(), pgxConfig) + if err != nil { + logger.Fatal("Error creating pgx pool", zap.Error(err)) + } + + trainingRepo := training.NewPostgresTrainingRepository(pool) + trainingDateRepo := training.NewPostgresTrainingDateRepository(pool) + trainingDateAttendeeRepo := training.NewPostgresTrainingDateAttendeeRepository(pool) + + apiHandlers := server.NewAPIHandlers(trainingRepo, trainingDateRepo, trainingDateAttendeeRepo) + + srv := server.NewServer(apiHandlers, port, logger, pool) + + return srv +} + +func BootstrapInMemory(logger *zap.Logger) *server.Server { + portStr := os.Getenv("APP_PORT") + if portStr == "" { + logger.Fatal("APP_PORT is not set") + } + port, err := strconv.Atoi(portStr) + if err != nil { + logger.Fatal("Error parsing APP_PORT", zap.Error(err)) + } + + trainingRepository := training.NewInMemoryTrainingRepository() + trainingDateRepository := training.NewInMemoryTrainingDateRepository() + trainingDateAttendeeRepository := training.NewInMemoryTrainingDateAttendeeRepository() + + f := faker.NewFaker(trainingRepository, trainingDateRepository) + if err := f.GenerateFakeData(); err != nil { + logger.Fatal("Error generating fake data", zap.Error(err)) + } + + apiHandlers := server.NewAPIHandlers(trainingRepository, trainingDateRepository, trainingDateAttendeeRepository) + srv := server.NewServer(apiHandlers, port, logger, nil) + + return srv +} diff --git a/internal/bootstrap/logger.go b/internal/bootstrap/logger.go new file mode 100644 index 0000000..92143fd --- /dev/null +++ b/internal/bootstrap/logger.go @@ -0,0 +1,23 @@ +package bootstrap + +import ( + "go.uber.org/zap" + "os" +) + +func Logger() *zap.Logger { + if os.Getenv("APP_ENV") == "development" { + logger, err := zap.NewDevelopment() + if err != nil { + return zap.NewNop() + } + return logger + } + + logger, err := zap.NewProduction() + if err != nil { + return zap.NewNop() + } + + return logger +} diff --git a/internal/faker/faker.go b/internal/faker/faker.go index fefe3a8..aa83591 100644 --- a/internal/faker/faker.go +++ b/internal/faker/faker.go @@ -207,11 +207,11 @@ func (f *Faker) GenerateFakeData() error { } now := time.Now() - min := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) - max := time.Date(now.Year()+1, now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) - delta := max.Sub(min) + minT := time.Date(now.Year(), now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) + maxT := time.Date(now.Year()+1, now.Month(), now.Day(), 8, 0, 0, 0, time.UTC) + delta := maxT.Sub(minT) - sec := rand.Int64N(int64(delta.Seconds())) + min.UnixNano()/1000000000 + sec := rand.Int64N(int64(delta.Seconds())) + minT.UnixNano()/1000000000 date := time.Unix(sec, 0) amount := decimal.NewFromInt(4900) @@ -231,21 +231,19 @@ func (f *Faker) GenerateFakeData() error { } td := training.TrainingDate{ - Date: date, - StartTime: date.Truncate(24 * time.Hour), - Days: t.Days, - IsOnline: online, - Location: location, - Address: "TBD", - Capacity: 12, - Price: money.Price{ - Amount: amount, - Currency: cur, - }, + Date: date, + StartTime: date.Truncate(24 * time.Hour), + Days: t.Days, + IsOnline: online, + Location: location, + Address: "TBD", + Capacity: 12, + PriceAmount: amount, + PriceCurrency: cur, } - err := f.trainingDateRepository.Create(t.ID, &td) - if err != nil { - return err + dateErr := f.trainingDateRepository.Create(t.ID, &td) + if dateErr != nil { + return dateErr } } } diff --git a/internal/money/money.go b/internal/money/money.go new file mode 100644 index 0000000..83b6b9a --- /dev/null +++ b/internal/money/money.go @@ -0,0 +1,8 @@ +package money + +import "github.com/shopspring/decimal" + +type Money struct { + Amount decimal.Decimal `db:"amount"` + Currency Currency `db:"currency"` +} diff --git a/internal/money/price.go b/internal/money/price.go deleted file mode 100644 index aea08f2..0000000 --- a/internal/money/price.go +++ /dev/null @@ -1,8 +0,0 @@ -package money - -import "github.com/shopspring/decimal" - -type Price struct { - Amount decimal.Decimal - Currency Currency -} diff --git a/internal/server/api.go b/internal/server/api.go index d0e105e..2635bbb 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -238,8 +238,8 @@ func (h *APIHandlers) ListTrainingDates(ctx context.Context, req ListTrainingDat Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, } } @@ -258,7 +258,7 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining }}, nil } - price := money.Price{ + price := money.Money{ Amount: amount, Currency: req.Body.Price.Currency, } @@ -274,14 +274,15 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining } td := training.TrainingDate{ - Date: req.Body.Date.Time, - StartTime: startTime, - Days: req.Body.Days, - IsOnline: req.Body.IsOnline, - Location: req.Body.Location, - Address: req.Body.Address, - Capacity: req.Body.Capacity, - Price: price, + Date: req.Body.Date.Time, + StartTime: startTime, + Days: req.Body.Days, + IsOnline: req.Body.IsOnline, + Location: req.Body.Location, + Address: req.Body.Address, + Capacity: req.Body.Capacity, + PriceAmount: price.Amount, + PriceCurrency: price.Currency, } err = h.trainingDateRepository.Create(req.TrainingID, &td) @@ -304,8 +305,8 @@ func (h *APIHandlers) CreateTrainingDate(ctx context.Context, req CreateTraining Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, }, nil } @@ -357,8 +358,8 @@ func (h *APIHandlers) GetTrainingDate(ctx context.Context, req GetTrainingDateRe Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, }, nil } @@ -374,7 +375,7 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining }}, nil } - price := money.Price{ + price := money.Money{ Amount: amount, Currency: req.Body.Price.Currency, } @@ -390,15 +391,16 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining } td := training.TrainingDate{ - ID: req.TrainingDateID, - Date: req.Body.Date.Time, - StartTime: startTime, - Days: req.Body.Days, - IsOnline: req.Body.IsOnline, - Location: req.Body.Location, - Address: req.Body.Address, - Capacity: req.Body.Capacity, - Price: price, + ID: req.TrainingDateID, + Date: req.Body.Date.Time, + StartTime: startTime, + Days: req.Body.Days, + IsOnline: req.Body.IsOnline, + Location: req.Body.Location, + Address: req.Body.Address, + Capacity: req.Body.Capacity, + PriceAmount: price.Amount, + PriceCurrency: price.Currency, } err = h.trainingDateRepository.Update(&td) @@ -421,8 +423,8 @@ func (h *APIHandlers) UpdateTrainingDate(ctx context.Context, req UpdateTraining Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, }, nil } @@ -463,8 +465,8 @@ func (h *APIHandlers) ListAllUpcomingTrainingDates(ctx context.Context, req List Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, } } @@ -496,8 +498,8 @@ func (h *APIHandlers) ListTrainingUpcomingDates(ctx context.Context, req ListTra Address: td.Address, Capacity: td.Capacity, Price: Price{ - Amount: td.Price.Amount.String(), - Currency: td.Price.Currency, + Amount: td.PriceAmount.String(), + Currency: td.PriceCurrency, }, } } @@ -529,8 +531,8 @@ func (h *APIHandlers) ListTrainingDateAttendees(ctx context.Context, req ListTra HasPaid: a.HasPaid, HasAttended: a.HasAttended, Bill: Price{ - Amount: a.Bill.Amount.String(), - Currency: a.Bill.Currency, + Amount: a.BillAmount.String(), + Currency: a.BillCurrency, }, } } @@ -576,15 +578,16 @@ func (h *APIHandlers) CreateTrainingDateAttendee(ctx context.Context, req Create } ta := training.TrainingDateAttendee{ - Name: req.Body.Name, - Email: string(req.Body.Email), - Phone: req.Body.Phone, - Company: req.Body.Company, - Position: req.Body.Position, - IsStudent: *req.Body.IsStudent, - HasPaid: false, - HasAttended: false, - Bill: td.Price, + Name: req.Body.Name, + Email: string(req.Body.Email), + Phone: req.Body.Phone, + Company: req.Body.Company, + Position: req.Body.Position, + IsStudent: *req.Body.IsStudent, + HasPaid: false, + HasAttended: false, + BillAmount: td.PriceAmount, + BillCurrency: td.PriceCurrency, } err = h.trainingDateAttendeeRepository.Create(req.TrainingDateID, &ta) @@ -608,8 +611,8 @@ func (h *APIHandlers) CreateTrainingDateAttendee(ctx context.Context, req Create HasAttended: ta.HasAttended, HasPaid: ta.HasPaid, Bill: Price{ - Amount: ta.Bill.Amount.String(), - Currency: ta.Bill.Currency, + Amount: ta.BillAmount.String(), + Currency: ta.BillCurrency, }, }, nil } @@ -714,8 +717,8 @@ func (h *APIHandlers) GetTrainingDateAttendee(ctx context.Context, req GetTraini HasAttended: ta.HasAttended, HasPaid: ta.HasPaid, Bill: Price{ - Amount: ta.Bill.Amount.String(), - Currency: ta.Bill.Currency, + Amount: ta.BillAmount.String(), + Currency: ta.BillCurrency, }, }, nil } @@ -803,8 +806,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendee(ctx context.Context, req Update HasAttended: ta.HasAttended, HasPaid: ta.HasPaid, Bill: Price{ - Amount: ta.Bill.Amount.String(), - Currency: ta.Bill.Currency, + Amount: ta.BillAmount.String(), + Currency: ta.BillCurrency, }, }, nil } @@ -887,8 +890,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendeePayment(ctx context.Context, req HasAttended: ta.HasAttended, HasPaid: ta.HasPaid, Bill: Price{ - Amount: ta.Bill.Amount.String(), - Currency: ta.Bill.Currency, + Amount: ta.BillAmount.String(), + Currency: ta.BillCurrency, }, }, nil } @@ -971,8 +974,8 @@ func (h *APIHandlers) UpdateTrainingDateAttendeeAttendance(ctx context.Context, HasAttended: ta.HasAttended, HasPaid: ta.HasPaid, Bill: Price{ - Amount: ta.Bill.Amount.String(), - Currency: ta.Bill.Currency, + Amount: ta.BillAmount.String(), + Currency: ta.BillCurrency, }, }, nil } diff --git a/internal/server/server.go b/internal/server/server.go index 059f6b9..1039147 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "github.com/jackc/pgx/v5/pgxpool" "time" fiberzap "github.com/gofiber/contrib/fiberzap/v2" @@ -16,13 +17,15 @@ type Server struct { port int logger *zap.Logger apiHandlers *APIHandlers + pool *pgxpool.Pool } -func NewServer(apiHandlers *APIHandlers, port int, logger *zap.Logger) *Server { +func NewServer(apiHandlers *APIHandlers, port int, logger *zap.Logger, pool *pgxpool.Pool) *Server { return &Server{ apiHandlers: apiHandlers, port: port, logger: logger, + pool: pool, } } @@ -54,6 +57,11 @@ func (s *Server) Run(ctx context.Context) { panic(err) } + // gracefully shutdown/close database pool + if s.pool != nil { + s.pool.Close() + } + s.logger.Info("HTTP server shut down gracefully.", zap.Duration("duration", time.Since(shutdownBegan))) } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 136911c..01502a7 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -338,7 +338,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -384,7 +384,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -447,7 +447,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -510,7 +510,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -584,7 +584,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -599,7 +599,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -614,7 +614,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -663,7 +663,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -705,7 +705,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -765,7 +765,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -782,7 +782,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -842,7 +842,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -856,7 +856,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -909,7 +909,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -923,7 +923,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -986,7 +986,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -1000,7 +1000,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -1046,7 +1046,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -1064,7 +1064,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -1123,7 +1123,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -1141,7 +1141,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, @@ -1200,7 +1200,7 @@ func TestServer(t *testing.T) { IsOnline: false, Location: "NYC", StartTime: date, - Price: money.Price{ + Price: money.Money{ Amount: decimal.NewFromInt(200), Currency: "EUR", }, @@ -1216,7 +1216,7 @@ func TestServer(t *testing.T) { Position: "Software Engineer", Phone: "+420 123 456 789", IsStudent: false, - Bill: money.Price{ + Bill: money.Money{ Amount: td.Price.Amount, Currency: td.Price.Currency, }, diff --git a/migrations/001_trainings.down.sql b/migrations/001_trainings.down.sql new file mode 100644 index 0000000..72e655d --- /dev/null +++ b/migrations/001_trainings.down.sql @@ -0,0 +1,9 @@ +DROP TABLE IF EXISTS training.date_attendees; + +DROP TABLE IF EXISTS training.dates; + +DROP TABLE IF EXISTS training.prices; + +DROP TABLE IF EXISTS training.trainings; + +DROP SCHEMA IF EXISTS training; diff --git a/migrations/001_trainings.up.sql b/migrations/001_trainings.up.sql new file mode 100644 index 0000000..acc9f85 --- /dev/null +++ b/migrations/001_trainings.up.sql @@ -0,0 +1,56 @@ +BEGIN; + +CREATE SCHEMA IF NOT EXISTS training; + +CREATE TABLE IF NOT EXISTS training.trainings ( + id UUID PRIMARY KEY, + name varchar(255) NOT NULL, + description text NOT NULL, + days smallint NOT NULL +); + +CREATE TABLE IF NOT EXISTS training.prices ( + training_id UUID REFERENCES training.trainings(id), + amount NUMERIC(10,4) NOT NULL, + currency VARCHAR(3) NOT NULL, + CONSTRAINT positive_amount CHECK (amount >= 0), + CONSTRAINT allowed_currencies CHECK (currency IN ('USD', 'EUR', 'CZK')), + type VARCHAR(10) NOT NULL, + CHECK (type IN ('OPEN', 'CORPORATE')), + PRIMARY KEY (training_id, currency, type) -- composite primary key +); + +CREATE TABLE IF NOT EXISTS training.dates ( + ID UUID PRIMARY KEY, + training_id UUID REFERENCES training.trainings(id), + date DATE NOT NULL, + start_time TIME NOT NULL, + days SMALLINT NOT NULL, + is_online BOOLEAN NOT NULL, + location VARCHAR(255) NOT NULL, + address VARCHAR(500) NOT NULL, + capacity SMALLINT NOT NULL, + price_amount NUMERIC(10,4) NOT NULL, + price_currency VARCHAR(3) NOT NULL, + CONSTRAINT positive_amount CHECK (price_amount >= 0), + CONSTRAINT allowed_currencies CHECK (price_currency IN ('USD', 'EUR', 'CZK')) +); + +CREATE TABLE IF NOT EXISTS training.date_attendees ( + id UUID PRIMARY KEY, + date_id UUID REFERENCES training.dates(id), + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL, + company VARCHAR(255) NOT NULL, + position VARCHAR(255) NOT NULL, + is_student BOOLEAN NOT NULL, + has_paid BOOLEAN NOT NULL, + has_attended BOOLEAN NOT NULL, + bill_amount NUMERIC(10,4) NOT NULL, + bill_currency VARCHAR(3) NOT NULL, + CONSTRAINT positive_amount CHECK (bill_amount >= 0), + CONSTRAINT allowed_currencies CHECK (bill_currency IN ('USD', 'EUR', 'CZK')) +); + +COMMIT; diff --git a/pkg/training/inmemory_repository.go b/pkg/training/inmemory_repository.go new file mode 100644 index 0000000..6b697ca --- /dev/null +++ b/pkg/training/inmemory_repository.go @@ -0,0 +1,270 @@ +package training + +import ( + "sync" + "time" +) + +type InMemoryTrainingRepository struct { + trainings map[TrainingID]Training + lock sync.RWMutex +} + +func NewInMemoryTrainingRepository() *InMemoryTrainingRepository { + return &InMemoryTrainingRepository{ + trainings: make(map[TrainingID]Training), + } +} + +func (r *InMemoryTrainingRepository) Create(training *Training) error { + r.lock.Lock() + defer r.lock.Unlock() + + training.ID = NewTrainingID() + r.trainings[training.ID] = *training + return nil +} + +func (r *InMemoryTrainingRepository) FindByID(id TrainingID) (*Training, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + training, ok := r.trainings[id] + if !ok { + return nil, ErrTrainingNotFound + } + return &training, nil +} + +func (r *InMemoryTrainingRepository) FindAll() ([]Training, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + trainings := make([]Training, 0, len(r.trainings)) + for _, training := range r.trainings { + trainings = append(trainings, training) + } + return trainings, nil +} + +func (r *InMemoryTrainingRepository) Update(training *Training) error { + r.lock.Lock() + defer r.lock.Unlock() + + r.trainings[training.ID] = *training + return nil +} + +func (r *InMemoryTrainingRepository) Delete(id TrainingID) error { + r.lock.Lock() + defer r.lock.Unlock() + + _, ok := r.trainings[id] + if !ok { + return ErrTrainingNotFound + } + + delete(r.trainings, id) + return nil +} + +type InMemoryTrainingDateRepository struct { + trainingDates map[TrainingDateID]TrainingDate + trainingToDates map[TrainingID][]TrainingDateID + + lock sync.RWMutex +} + +func NewInMemoryTrainingDateRepository() *InMemoryTrainingDateRepository { + return &InMemoryTrainingDateRepository{ + trainingDates: make(map[TrainingDateID]TrainingDate), + trainingToDates: make(map[TrainingID][]TrainingDateID), + } +} + +func (r *InMemoryTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error { + r.lock.Lock() + defer r.lock.Unlock() + + trainingDate.ID = NewTrainingDateID() + trainingDate.trainingID = trainingID + + r.trainingDates[trainingDate.ID] = *trainingDate + r.trainingToDates[trainingID] = append(r.trainingToDates[trainingID], trainingDate.ID) + return nil +} + +func (r *InMemoryTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + date, ok := r.trainingDates[id] + if !ok { + return nil, ErrTrainingDateNotFound + } + return &date, nil +} + +func (r *InMemoryTrainingDateRepository) FindAll() ([]TrainingDate, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + dates := make([]TrainingDate, len(r.trainingDates)) + for _, date := range r.trainingDates { + dates = append(dates, date) + } + + return dates, nil +} + +func (r *InMemoryTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + dates := make([]TrainingDate, 0) + for _, id := range r.trainingToDates[trainingID] { + dates = append(dates, r.trainingDates[id]) + } + + return dates, nil +} + +func (r *InMemoryTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + now := time.Now() + var dates []TrainingDate + for _, id := range r.trainingToDates[trainingID] { + date := r.trainingDates[id] + if date.Date.After(now) { + dates = append(dates, date) + } + } + + return dates, nil +} + +func (r *InMemoryTrainingDateRepository) Update(trainingDate *TrainingDate) error { + r.lock.Lock() + defer r.lock.Unlock() + + oldDate := r.trainingDates[trainingDate.ID] + trainingDate.trainingID = oldDate.trainingID + r.trainingDates[trainingDate.ID] = *trainingDate + return nil +} + +func (r *InMemoryTrainingDateRepository) Delete(id TrainingDateID) error { + r.lock.Lock() + defer r.lock.Unlock() + + date, ok := r.trainingDates[id] + if !ok { + return ErrTrainingDateNotFound + } + + delete(r.trainingDates, id) + dates := r.trainingToDates[date.trainingID] + for idx, d := range dates { + if d == id { + r.trainingToDates[date.trainingID] = append(dates[:idx], dates[idx+1:]...) + break + } + } + + return nil +} + +type InMemoryTrainingDateAttendeeRepository struct { + attendees map[TrainingDateAttendeeID]TrainingDateAttendee + dateToAttendees map[TrainingDateID][]TrainingDateAttendeeID + + lock sync.RWMutex +} + +func NewInMemoryTrainingDateAttendeeRepository() *InMemoryTrainingDateAttendeeRepository { + return &InMemoryTrainingDateAttendeeRepository{ + attendees: make(map[TrainingDateAttendeeID]TrainingDateAttendee), + dateToAttendees: make(map[TrainingDateID][]TrainingDateAttendeeID), + } +} + +func (r *InMemoryTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error { + r.lock.Lock() + defer r.lock.Unlock() + + attendee.ID = NewTrainingDateAttendeeID() + attendee.trainingDateID = trainingDateID + + r.attendees[attendee.ID] = *attendee + r.dateToAttendees[trainingDateID] = append(r.dateToAttendees[trainingDateID], attendee.ID) + + return nil +} + +func (r *InMemoryTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + attendee, ok := r.attendees[id] + if !ok { + return nil, ErrTrainingDateAttendeeNotFound + } + return &attendee, nil +} + +func (r *InMemoryTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + attendees := make([]TrainingDateAttendee, len(r.attendees)) + for _, attendee := range r.attendees { + attendees = append(attendees, attendee) + } + + return attendees, nil +} + +func (r *InMemoryTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + attendees := make([]TrainingDateAttendee, 0) + for _, id := range r.dateToAttendees[trainingDateID] { + attendees = append(attendees, r.attendees[id]) + } + + return attendees, nil +} + +func (r *InMemoryTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error { + r.lock.Lock() + defer r.lock.Unlock() + + oldAttendee := r.attendees[attendee.ID] + attendee.trainingDateID = oldAttendee.trainingDateID + r.attendees[attendee.ID] = *attendee + return nil +} + +func (r *InMemoryTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error { + r.lock.Lock() + defer r.lock.Unlock() + + attendee, ok := r.attendees[id] + if !ok { + return ErrTrainingDateAttendeeNotFound + } + + delete(r.attendees, id) + attendees := r.dateToAttendees[attendee.trainingDateID] + for idx, a := range attendees { + if a == id { + r.dateToAttendees[attendee.trainingDateID] = append(attendees[:idx], attendees[idx+1:]...) + break + } + } + + return nil +} diff --git a/pkg/training/model.go b/pkg/training/model.go index 7df1894..6b1a3e6 100644 --- a/pkg/training/model.go +++ b/pkg/training/model.go @@ -1,11 +1,11 @@ package training import ( + "github.com/shopspring/decimal" + "gitlab.mareshq.com/hq/yggdrasil/internal/money" "time" "github.com/google/uuid" - "github.com/shopspring/decimal" - "gitlab.mareshq.com/hq/yggdrasil/internal/money" ) type TrainingID = uuid.UUID @@ -19,7 +19,7 @@ type Training struct { Name string Days int8 Description string - Pricing []TrainingPrice + Pricing []TrainingPrice `db:"-"` } type TrainingPrice struct { @@ -44,15 +44,16 @@ func NewTrainingDateID() TrainingDateID { type TrainingDate struct { trainingID TrainingID - ID TrainingDateID - Date time.Time - StartTime time.Time - Days int8 - IsOnline bool - Location string - Address string - Capacity int8 - Price money.Price + ID TrainingDateID + Date time.Time + StartTime time.Time + Days int8 + IsOnline bool + Location string + Address string + Capacity int8 + PriceAmount decimal.Decimal `db:"price_amount"` + PriceCurrency money.Currency `db:"price_currency"` } type TrainingDateAttendeeID = uuid.UUID @@ -64,14 +65,15 @@ func NewTrainingDateAttendeeID() TrainingDateAttendeeID { type TrainingDateAttendee struct { trainingDateID TrainingDateID - ID TrainingDateAttendeeID - Name string - Email string - Phone string - Company string - Position string - Bill money.Price - IsStudent bool - HasPaid bool - HasAttended bool + ID TrainingDateAttendeeID + Name string + Email string + Phone string + Company string + Position string + IsStudent bool + HasPaid bool + HasAttended bool + BillAmount decimal.Decimal `db:"bill_amount"` + BillCurrency money.Currency `db:"bill_currency"` } diff --git a/pkg/training/postgres_repository.go b/pkg/training/postgres_repository.go new file mode 100644 index 0000000..c475e22 --- /dev/null +++ b/pkg/training/postgres_repository.go @@ -0,0 +1,561 @@ +package training + +import ( + "context" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "time" +) + +type PostgresTrainingRepository struct { + pg *pgxpool.Pool +} + +func NewPostgresTrainingRepository(pg *pgxpool.Pool) *PostgresTrainingRepository { + return &PostgresTrainingRepository{pg: pg} +} + +func (r *PostgresTrainingRepository) Create(training *Training) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + training.ID = NewTrainingID() + + tx, err := r.pg.Begin(ctx) + if err != nil { + return err + } + + _, err = tx.Exec(ctx, ` + INSERT INTO training.trainings (id, name, description, days) + VALUES ($1, $2, $3, $4) + `, training.ID, training.Name, training.Description, training.Days) + + if err != nil { + return err + } + + queryBatch := &pgx.Batch{} + + for _, price := range training.Pricing { + queryBatch.Queue(` + INSERT INTO training.prices (training_id, amount, currency, type) + VALUES ($1, $2, $3, $4) + `, training.ID, price.Amount, price.Currency, price.Type) + } + + batch := tx.SendBatch(ctx, queryBatch) + defer func() { + if e := batch.Close(); e != nil { + err = e + } + + if err != nil { + _ = tx.Rollback(ctx) + } else { + if e := tx.Commit(ctx); e != nil { + err = e + } + } + }() + + _, err = batch.Exec() + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingRepository) FindByID(id TrainingID) (*Training, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var training Training + err := r.pg.QueryRow(ctx, ` + SELECT id, name, description, days + FROM training.trainings + WHERE id = $1 + `, id).Scan(&training.ID, &training.Name, &training.Description, &training.Days) + + if err != nil { + return nil, err + } + + rows, err := r.pg.Query(ctx, ` + SELECT amount, currency, type + FROM training.prices + WHERE training_id = $1 + `, id) + if err != nil { + return nil, err + } + defer rows.Close() + + training.Pricing = make([]TrainingPrice, 0) + training.Pricing, err = pgx.CollectRows[TrainingPrice](rows, func(row pgx.CollectableRow) (TrainingPrice, error) { + var price TrainingPrice + err := row.Scan(&price.Amount, &price.Currency, &price.Type) + if err != nil { + return TrainingPrice{}, err + } + return price, nil + }) + if err != nil { + return nil, err + } + + return &training, nil +} + +func (r *PostgresTrainingRepository) FindAll() ([]Training, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, ` + SELECT * + FROM training.trainings + `) + if err != nil { + return nil, err + } + defer rows.Close() + + trainings, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (Training, error) { + var training Training + scanErr := row.Scan(&training.ID, &training.Name, &training.Description, &training.Days) + if scanErr != nil { + return Training{}, scanErr + } + + priceRows, queryErr := r.pg.Query(ctx, `SELECT amount, currency, type FROM training.prices WHERE training_id = $1`, training.ID) + if queryErr != nil { + return Training{}, queryErr + } + + training.Pricing, scanErr = pgx.CollectRows(priceRows, pgx.RowToStructByName[TrainingPrice]) + if scanErr != nil { + return Training{}, scanErr + } + + return training, nil + }) + + if err != nil { + return nil, err + } + + return trainings, nil +} + +func (r *PostgresTrainingRepository) Update(training *Training) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + tx, err := r.pg.Begin(ctx) + if err != nil { + return err + } + + _, err = tx.Exec(ctx, ` + UPDATE training.trainings + SET name = $1, description = $2, days = $3 + WHERE id = $4 + `, training.Name, training.Description, training.Days, training.ID) + + if err != nil { + return err + } + + queryBatch := &pgx.Batch{} + + for _, price := range training.Pricing { + queryBatch.Queue(` + INSERT INTO training.prices (training_id, amount, currency, type) + VALUES ($1, $2, $3, $4) + `, training.ID, price.Amount, price.Currency, price.Type) + } + + batch := tx.SendBatch(ctx, queryBatch) + defer func() { + if e := batch.Close(); e != nil { + err = e + } + + if err != nil { + _ = tx.Rollback(ctx) + } else { + if e := tx.Commit(ctx); e != nil { + err = e + } + } + }() + + _, err = batch.Exec() + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingRepository) Delete(id TrainingID) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + tx, err := r.pg.Begin(ctx) + if err != nil { + return err + } + + _, err = tx.Exec(context.Background(), ` + DELETE FROM training.prices + WHERE training_id = $1 + `, id) + + if err != nil { + return err + } + + _, err = tx.Exec(context.Background(), ` + DELETE FROM training.trainings + WHERE id = $1 + `, id) + + if err != nil { + return err + } + + err = tx.Commit(ctx) + if err != nil { + return err + } + + return nil +} + +type PostgresTrainingDateRepository struct { + pg *pgxpool.Pool +} + +func NewPostgresTrainingDateRepository(pg *pgxpool.Pool) *PostgresTrainingDateRepository { + return &PostgresTrainingDateRepository{pg: pg} +} + +func (r *PostgresTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + trainingDate.ID = NewTrainingDateID() + + _, err := r.pg.Exec(ctx, ` + INSERT INTO training.dates (id, training_id, date, start_time, days, is_online, location, address, capacity, price_amount, price_currency) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + trainingDate.ID, + trainingID, + trainingDate.Date, + trainingDate.StartTime, + trainingDate.Days, + trainingDate.IsOnline, + trainingDate.Location, + trainingDate.Address, + trainingDate.Capacity, + trainingDate.PriceAmount, + trainingDate.PriceCurrency, + ) + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var trainingDate TrainingDate + err := r.pg.QueryRow(ctx, `SELECT * FROM training.dates WHERE id = $1`, id). + Scan( + &trainingDate.ID, + &trainingDate.trainingID, + &trainingDate.Date, + &trainingDate.StartTime, + &trainingDate.Days, + &trainingDate.IsOnline, + &trainingDate.Location, + &trainingDate.Address, + &trainingDate.Capacity, + &trainingDate.PriceAmount, + &trainingDate.PriceCurrency, + ) + if err != nil { + return nil, err + } + + return &trainingDate, nil +} + +func (r *PostgresTrainingDateRepository) FindAll() ([]TrainingDate, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates`) + if err != nil { + return nil, err + } + defer rows.Close() + + var trainingDates []TrainingDate + for rows.Next() { + var trainingDate TrainingDate + err := rows.Scan( + &trainingDate.ID, + &trainingDate.trainingID, + &trainingDate.Date, + &trainingDate.StartTime, + &trainingDate.Days, + &trainingDate.IsOnline, + &trainingDate.Location, + &trainingDate.Address, + &trainingDate.Capacity, + &trainingDate.PriceAmount, + &trainingDate.PriceCurrency, + ) + if err != nil { + return nil, err + } + + trainingDates = append(trainingDates, trainingDate) + } + + return trainingDates, nil +} + +func (r *PostgresTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, `SELECT * FROM training.dates WHERE training_id = $1`, trainingID) + if err != nil { + return nil, err + } + defer rows.Close() + + var trainingDates []TrainingDate + for rows.Next() { + var trainingDate TrainingDate + err := rows.Scan( + &trainingDate.ID, + &trainingDate.trainingID, + &trainingDate.Date, + &trainingDate.StartTime, + &trainingDate.Days, + &trainingDate.IsOnline, + &trainingDate.Location, + &trainingDate.Address, + &trainingDate.Capacity, + &trainingDate.PriceAmount, + &trainingDate.PriceCurrency, + ) + if err != nil { + return nil, err + } + + trainingDates = append(trainingDates, trainingDate) + } + + return trainingDates, nil +} + +func (r *PostgresTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, ` + SELECT * FROM training.dates + WHERE training_id = $1 AND date > $2 + `, trainingID, time.Now()) + if err != nil { + return nil, err + } + defer rows.Close() + + var trainingDates []TrainingDate + for rows.Next() { + var trainingDate TrainingDate + err := rows.Scan( + &trainingDate.ID, + &trainingDate.trainingID, + &trainingDate.Date, + &trainingDate.StartTime, + &trainingDate.Days, + &trainingDate.IsOnline, + &trainingDate.Location, + &trainingDate.Address, + &trainingDate.Capacity, + &trainingDate.PriceAmount, + &trainingDate.PriceCurrency, + ) + if err != nil { + return nil, err + } + + trainingDates = append(trainingDates, trainingDate) + } + + return trainingDates, nil +} + +func (r *PostgresTrainingDateRepository) Update(trainingDate *TrainingDate) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := r.pg.Exec(ctx, ` + UPDATE training.dates + SET date = $1, start_time = $2, days = $3, is_online = $4, location = $5, address = $6, capacity = $7, price_amount = $8, price_currency = $9 + WHERE id = $10 + `, trainingDate.Date, trainingDate.StartTime, trainingDate.Days, trainingDate.IsOnline, trainingDate.Location, trainingDate.Address, trainingDate.Capacity, trainingDate.PriceAmount, trainingDate.PriceCurrency, trainingDate.ID) + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingDateRepository) Delete(id TrainingDateID) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := r.pg.Exec(ctx, `DELETE FROM training.dates WHERE id = $1`, id) + if err != nil { + return err + } + + return nil +} + +type PostgresTrainingDateAttendeeRepository struct { + pg *pgxpool.Pool +} + +func NewPostgresTrainingDateAttendeeRepository(pg *pgxpool.Pool) *PostgresTrainingDateAttendeeRepository { + return &PostgresTrainingDateAttendeeRepository{pg: pg} +} + +func (r *PostgresTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + attendee.ID = NewTrainingDateAttendeeID() + + _, err := r.pg.Exec(ctx, ` + INSERT INTO training.date_attendees (id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + `, attendee.ID, trainingDateID, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency) + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var attendee TrainingDateAttendee + err := r.pg.QueryRow(ctx, ` + SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency + FROM training.date_attendees + WHERE id = $1 + `, id).Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency) + if err != nil { + return nil, err + } + + return &attendee, nil +} + +func (r *PostgresTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, ` + SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency + FROM training.date_attendees + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var attendees []TrainingDateAttendee + for rows.Next() { + var attendee TrainingDateAttendee + err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency) + if err != nil { + return nil, err + } + + attendees = append(attendees, attendee) + } + + return attendees, nil +} + +func (r *PostgresTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + rows, err := r.pg.Query(ctx, ` + SELECT id, date_id, name, email, phone, company, position, is_student, has_paid, has_attended, bill_amount, bill_currency + FROM training.date_attendees + WHERE date_id = $1 + `, trainingDateID) + if err != nil { + return nil, err + } + defer rows.Close() + + var attendees []TrainingDateAttendee + for rows.Next() { + var attendee TrainingDateAttendee + err := rows.Scan(&attendee.ID, &attendee.trainingDateID, &attendee.Name, &attendee.Email, &attendee.Phone, &attendee.Company, &attendee.Position, &attendee.IsStudent, &attendee.HasPaid, &attendee.HasAttended, &attendee.BillAmount, &attendee.BillCurrency) + if err != nil { + return nil, err + } + + attendees = append(attendees, attendee) + } + + return attendees, nil +} + +func (r *PostgresTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := r.pg.Exec(ctx, ` + UPDATE training.date_attendees + SET name = $1, email = $2, phone = $3, company = $4, position = $5, is_student = $6, has_paid = $7, has_attended = $8, bill_amount = $9, bill_currency = $10 + WHERE id = $10 + `, attendee.Name, attendee.Email, attendee.Phone, attendee.Company, attendee.Position, attendee.IsStudent, attendee.HasPaid, attendee.HasAttended, attendee.BillAmount, attendee.BillCurrency, attendee.ID) + if err != nil { + return err + } + + return nil +} + +func (r *PostgresTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := r.pg.Exec(ctx, `DELETE FROM training.date_attendees WHERE id = $1`, id) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/training/repository.go b/pkg/training/repository.go index 81fa595..0e78c35 100644 --- a/pkg/training/repository.go +++ b/pkg/training/repository.go @@ -1,10 +1,5 @@ package training -import ( - "sync" - "time" -) - type TrainingRepository interface { Create(training *Training) error FindByID(id TrainingID) (*Training, error) @@ -13,69 +8,6 @@ type TrainingRepository interface { Delete(id TrainingID) error } -type InMemoryTrainingRepository struct { - trainings map[TrainingID]Training - lock sync.RWMutex -} - -func NewInMemoryTrainingRepository() *InMemoryTrainingRepository { - return &InMemoryTrainingRepository{ - trainings: make(map[TrainingID]Training), - } -} - -func (r *InMemoryTrainingRepository) Create(training *Training) error { - r.lock.Lock() - defer r.lock.Unlock() - - training.ID = NewTrainingID() - r.trainings[training.ID] = *training - return nil -} - -func (r *InMemoryTrainingRepository) FindByID(id TrainingID) (*Training, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - training, ok := r.trainings[id] - if !ok { - return nil, ErrTrainingNotFound - } - return &training, nil -} - -func (r *InMemoryTrainingRepository) FindAll() ([]Training, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - trainings := make([]Training, 0, len(r.trainings)) - for _, training := range r.trainings { - trainings = append(trainings, training) - } - return trainings, nil -} - -func (r *InMemoryTrainingRepository) Update(training *Training) error { - r.lock.Lock() - defer r.lock.Unlock() - - r.trainings[training.ID] = *training - return nil -} - -func (r *InMemoryTrainingRepository) Delete(id TrainingID) error { - r.lock.Lock() - defer r.lock.Unlock() - - _, ok := r.trainings[id] - if !ok { - return ErrTrainingNotFound - } - - delete(r.trainings, id) - return nil -} - type TrainingDateRepository interface { Create(trainingID TrainingID, trainingDate *TrainingDate) error FindByID(id TrainingDateID) (*TrainingDate, error) @@ -86,114 +18,6 @@ type TrainingDateRepository interface { Delete(id TrainingDateID) error } -type InMemoryTrainingDateRepository struct { - trainingDates map[TrainingDateID]TrainingDate - trainingToDates map[TrainingID][]TrainingDateID - - lock sync.RWMutex -} - -func NewInMemoryTrainingDateRepository() *InMemoryTrainingDateRepository { - return &InMemoryTrainingDateRepository{ - trainingDates: make(map[TrainingDateID]TrainingDate), - trainingToDates: make(map[TrainingID][]TrainingDateID), - } -} - -func (r *InMemoryTrainingDateRepository) Create(trainingID TrainingID, trainingDate *TrainingDate) error { - r.lock.Lock() - defer r.lock.Unlock() - - trainingDate.ID = NewTrainingDateID() - trainingDate.trainingID = trainingID - - r.trainingDates[trainingDate.ID] = *trainingDate - r.trainingToDates[trainingID] = append(r.trainingToDates[trainingID], trainingDate.ID) - return nil -} - -func (r *InMemoryTrainingDateRepository) FindByID(id TrainingDateID) (*TrainingDate, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - date, ok := r.trainingDates[id] - if !ok { - return nil, ErrTrainingDateNotFound - } - return &date, nil -} - -func (r *InMemoryTrainingDateRepository) FindAll() ([]TrainingDate, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - dates := make([]TrainingDate, len(r.trainingDates)) - for _, date := range r.trainingDates { - dates = append(dates, date) - } - - return dates, nil -} - -func (r *InMemoryTrainingDateRepository) FindAllByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - dates := make([]TrainingDate, 0) - for _, id := range r.trainingToDates[trainingID] { - dates = append(dates, r.trainingDates[id]) - } - - return dates, nil -} - -func (r *InMemoryTrainingDateRepository) FindUpcomingByTrainingID(trainingID TrainingID) ([]TrainingDate, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - now := time.Now() - var dates []TrainingDate - for _, id := range r.trainingToDates[trainingID] { - date := r.trainingDates[id] - if date.Date.After(now) { - dates = append(dates, date) - } - } - - return dates, nil -} - -func (r *InMemoryTrainingDateRepository) Update(trainingDate *TrainingDate) error { - r.lock.Lock() - defer r.lock.Unlock() - - oldDate := r.trainingDates[trainingDate.ID] - trainingDate.trainingID = oldDate.trainingID - r.trainingDates[trainingDate.ID] = *trainingDate - return nil -} - -func (r *InMemoryTrainingDateRepository) Delete(id TrainingDateID) error { - r.lock.Lock() - defer r.lock.Unlock() - - date, ok := r.trainingDates[id] - if !ok { - return ErrTrainingDateNotFound - } - - delete(r.trainingDates, id) - dates := r.trainingToDates[date.trainingID] - for idx, d := range dates { - if d == id { - r.trainingToDates[date.trainingID] = append(dates[:idx], dates[idx+1:]...) - break - } - } - - return nil -} - type TrainingDateAttendeeRepository interface { Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) @@ -202,96 +26,3 @@ type TrainingDateAttendeeRepository interface { Update(attendee *TrainingDateAttendee) error Delete(id TrainingDateAttendeeID) error } - -type InMemoryTrainingDateAttendeeRepository struct { - attendees map[TrainingDateAttendeeID]TrainingDateAttendee - dateToAttendees map[TrainingDateID][]TrainingDateAttendeeID - - lock sync.RWMutex -} - -func NewInMemoryTrainingDateAttendeeRepository() *InMemoryTrainingDateAttendeeRepository { - return &InMemoryTrainingDateAttendeeRepository{ - attendees: make(map[TrainingDateAttendeeID]TrainingDateAttendee), - dateToAttendees: make(map[TrainingDateID][]TrainingDateAttendeeID), - } -} - -func (r *InMemoryTrainingDateAttendeeRepository) Create(trainingDateID TrainingDateID, attendee *TrainingDateAttendee) error { - r.lock.Lock() - defer r.lock.Unlock() - - attendee.ID = NewTrainingDateAttendeeID() - attendee.trainingDateID = trainingDateID - - r.attendees[attendee.ID] = *attendee - r.dateToAttendees[trainingDateID] = append(r.dateToAttendees[trainingDateID], attendee.ID) - - return nil -} - -func (r *InMemoryTrainingDateAttendeeRepository) FindByID(id TrainingDateAttendeeID) (*TrainingDateAttendee, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - attendee, ok := r.attendees[id] - if !ok { - return nil, ErrTrainingDateAttendeeNotFound - } - return &attendee, nil -} - -func (r *InMemoryTrainingDateAttendeeRepository) FindAll() ([]TrainingDateAttendee, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - attendees := make([]TrainingDateAttendee, len(r.attendees)) - for _, attendee := range r.attendees { - attendees = append(attendees, attendee) - } - - return attendees, nil -} - -func (r *InMemoryTrainingDateAttendeeRepository) FindAllByTrainingDateID(trainingDateID TrainingDateID) ([]TrainingDateAttendee, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - attendees := make([]TrainingDateAttendee, 0) - for _, id := range r.dateToAttendees[trainingDateID] { - attendees = append(attendees, r.attendees[id]) - } - - return attendees, nil -} - -func (r *InMemoryTrainingDateAttendeeRepository) Update(attendee *TrainingDateAttendee) error { - r.lock.Lock() - defer r.lock.Unlock() - - oldAttendee := r.attendees[attendee.ID] - attendee.trainingDateID = oldAttendee.trainingDateID - r.attendees[attendee.ID] = *attendee - return nil -} - -func (r *InMemoryTrainingDateAttendeeRepository) Delete(id TrainingDateAttendeeID) error { - r.lock.Lock() - defer r.lock.Unlock() - - attendee, ok := r.attendees[id] - if !ok { - return ErrTrainingDateAttendeeNotFound - } - - delete(r.attendees, id) - attendees := r.dateToAttendees[attendee.trainingDateID] - for idx, a := range attendees { - if a == id { - r.dateToAttendees[attendee.trainingDateID] = append(attendees[:idx], attendees[idx+1:]...) - break - } - } - - return nil -}