From e98630507809492b16496f4370dd26b1d26220d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Sm=C3=B3=C5=82ka?= Date: Tue, 1 Sep 2020 22:11:59 +0200 Subject: [PATCH] Introduce Clean Architecture --- Makefile | 8 +- internal/common/auth/http.go | 4 +- internal/common/errors/errors.go | 53 ++++ internal/common/server/httperr/http_error.go | 18 ++ .../adapters/dates_firestore_repository.go | 99 ++++++ .../hour_firestore_repository.go | 2 +- .../{ => adapters}/hour_memory_repository.go | 2 +- .../{ => adapters}/hour_mysql_repository.go | 2 +- .../{ => adapters}/hour_repository_test.go | 21 +- internal/trainer/{model.go => app/date.go} | 39 ++- internal/trainer/app/hour_service.go | 78 +++++ internal/trainer/firestore.go | 78 ----- internal/trainer/fixtures.go | 17 +- internal/trainer/main.go | 20 +- internal/trainer/{ => ports}/grpc.go | 12 +- internal/trainer/{ => ports}/http.go | 64 ++-- .../trainer/{ => ports}/openapi_api.gen.go | 7 +- .../trainer/{ => ports}/openapi_types.gen.go | 7 +- internal/trainings/adapters/trainer_grpc.go | 45 +++ .../trainings_firestore_repository.go | 218 +++++++++++++ internal/trainings/adapters/users_grpc.go | 24 ++ internal/trainings/app/training.go | 23 ++ internal/trainings/app/training_service.go | 198 ++++++++++++ .../trainings/app/training_service_test.go | 194 ++++++++++++ internal/trainings/firestore.go | 296 ------------------ internal/trainings/go.mod | 1 + internal/trainings/main.go | 11 +- internal/trainings/{ => ports}/http.go | 85 +++-- .../trainings/{ => ports}/openapi_api.gen.go | 7 +- .../{ => ports}/openapi_types.gen.go | 4 +- internal/users/firestore.go | 4 +- internal/users/http.go | 2 +- 32 files changed, 1124 insertions(+), 519 deletions(-) create mode 100644 internal/common/errors/errors.go create mode 100644 internal/trainer/adapters/dates_firestore_repository.go rename internal/trainer/{ => adapters}/hour_firestore_repository.go (99%) rename internal/trainer/{ => adapters}/hour_memory_repository.go (98%) rename internal/trainer/{ => adapters}/hour_mysql_repository.go (99%) rename internal/trainer/{ => adapters}/hour_repository_test.go (94%) rename internal/trainer/{model.go => app/date.go} (51%) create mode 100644 internal/trainer/app/hour_service.go delete mode 100644 internal/trainer/firestore.go rename internal/trainer/{ => ports}/grpc.go (93%) rename internal/trainer/{ => ports}/http.go (59%) rename internal/trainer/{ => ports}/openapi_api.gen.go (97%) rename internal/trainer/{ => ports}/openapi_types.gen.go (94%) create mode 100644 internal/trainings/adapters/trainer_grpc.go create mode 100644 internal/trainings/adapters/trainings_firestore_repository.go create mode 100644 internal/trainings/adapters/users_grpc.go create mode 100644 internal/trainings/app/training.go create mode 100644 internal/trainings/app/training_service.go create mode 100644 internal/trainings/app/training_service_test.go delete mode 100644 internal/trainings/firestore.go rename internal/trainings/{ => ports}/http.go (57%) rename internal/trainings/{ => ports}/openapi_api.gen.go (98%) rename internal/trainings/{ => ports}/openapi_types.gen.go (94%) diff --git a/Makefile b/Makefile index e3a7f8e..9274d01 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,11 @@ openapi: openapi_http openapi_js .PHONY: openapi_http openapi_http: - oapi-codegen -generate types -o internal/trainings/openapi_types.gen.go -package main api/openapi/trainings.yml - oapi-codegen -generate chi-server -o internal/trainings/openapi_api.gen.go -package main api/openapi/trainings.yml + oapi-codegen -generate types -o internal/trainings/ports/openapi_types.gen.go -package ports api/openapi/trainings.yml + oapi-codegen -generate chi-server -o internal/trainings/ports/openapi_api.gen.go -package ports api/openapi/trainings.yml - oapi-codegen -generate types -o internal/trainer/openapi_types.gen.go -package main api/openapi/trainer.yml - oapi-codegen -generate chi-server -o internal/trainer/openapi_api.gen.go -package main api/openapi/trainer.yml + oapi-codegen -generate types -o internal/trainer/ports/openapi_types.gen.go -package ports api/openapi/trainer.yml + oapi-codegen -generate chi-server -o internal/trainer/ports/openapi_api.gen.go -package ports api/openapi/trainer.yml oapi-codegen -generate types -o internal/users/openapi_types.gen.go -package main api/openapi/users.yml oapi-codegen -generate chi-server -o internal/users/openapi_api.gen.go -package main api/openapi/users.yml diff --git a/internal/common/auth/http.go b/internal/common/auth/http.go index dcdd12f..6474096 100644 --- a/internal/common/auth/http.go +++ b/internal/common/auth/http.go @@ -6,8 +6,8 @@ import ( "strings" "firebase.google.com/go/auth" + commonerrors "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" - "github.com/pkg/errors" ) type FirebaseHttpMiddleware struct { @@ -71,7 +71,7 @@ const ( var ( // if we expect that the user of the function may be interested with concrete error, // it's a good idea to provide variable with this error - NoUserInContextError = errors.New("no user in context") + NoUserInContextError = commonerrors.NewAuthorizationError("no user in context", "no-user-found") ) func UserFromCtx(ctx context.Context) (User, error) { diff --git a/internal/common/errors/errors.go b/internal/common/errors/errors.go new file mode 100644 index 0000000..bc317e3 --- /dev/null +++ b/internal/common/errors/errors.go @@ -0,0 +1,53 @@ +package errors + +type ErrorType struct { + t string +} + +var ( + ErrorTypeUnknown = ErrorType{"unknown"} + ErrorTypeAuthorization = ErrorType{"authorization"} + ErrorTypeIncorrectInput = ErrorType{"incorrect-input"} +) + +type SlugError struct { + error string + slug string + errorType ErrorType +} + +func (s SlugError) Error() string { + return s.error +} + +func (s SlugError) Slug() string { + return s.slug +} + +func (s SlugError) ErrorType() ErrorType { + return s.errorType +} + +func NewSlugError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeUnknown, + } +} + +func NewAuthorizationError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeAuthorization, + } +} + +func NewIncorrectInputError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeIncorrectInput, + } +} diff --git a/internal/common/server/httperr/http_error.go b/internal/common/server/httperr/http_error.go index 0166524..8ca82a4 100644 --- a/internal/common/server/httperr/http_error.go +++ b/internal/common/server/httperr/http_error.go @@ -3,6 +3,7 @@ package httperr import ( "net/http" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" "github.com/go-chi/render" ) @@ -19,6 +20,23 @@ func BadRequest(slug string, err error, w http.ResponseWriter, r *http.Request) httpRespondWithError(err, slug, w, r, "Bad request", http.StatusBadRequest) } +func RespondWithSlugError(err error, w http.ResponseWriter, r *http.Request) { + slugError, ok := err.(errors.SlugError) + if !ok { + InternalError("internal-server-error", err, w, r) + return + } + + switch slugError.ErrorType() { + case errors.ErrorTypeAuthorization: + Unauthorised(slugError.Slug(), slugError, w, r) + case errors.ErrorTypeIncorrectInput: + BadRequest(slugError.Slug(), slugError, w, r) + default: + InternalError(slugError.Slug(), slugError, w, r) + } +} + func httpRespondWithError(err error, slug string, w http.ResponseWriter, r *http.Request, logMSg string, status int) { logs.GetLogEntry(r).WithError(err).WithField("error-slug", slug).Warn(logMSg) resp := ErrorResponse{slug, status} diff --git a/internal/trainer/adapters/dates_firestore_repository.go b/internal/trainer/adapters/dates_firestore_repository.go new file mode 100644 index 0000000..faa3e06 --- /dev/null +++ b/internal/trainer/adapters/dates_firestore_repository.go @@ -0,0 +1,99 @@ +package adapters + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + + "cloud.google.com/go/firestore" + "google.golang.org/api/iterator" +) + +type DateModel struct { + Date time.Time `firestore:"Date"` + HasFreeHours bool `firestore:"HasFreeHours"` + Hours []HourModel `firestore:"Hours"` +} + +type HourModel struct { + Available bool `firestore:"Available"` + HasTrainingScheduled bool `firestore:"HasTrainingScheduled"` + Hour time.Time `firestore:"Hour"` +} + +type DatesFirestoreRepository struct { + firestoreClient *firestore.Client +} + +func NewDatesFirestoreRepository(firestoreClient *firestore.Client) DatesFirestoreRepository { + if firestoreClient == nil { + panic("missing firestoreClient") + } + + return DatesFirestoreRepository{ + firestoreClient: firestoreClient, + } +} + +func (d DatesFirestoreRepository) trainerHoursCollection() *firestore.CollectionRef { + return d.firestoreClient.Collection("trainer-hours") +} + +func (d DatesFirestoreRepository) DocumentRef(dateTimeToUpdate time.Time) *firestore.DocumentRef { + return d.trainerHoursCollection().Doc(dateTimeToUpdate.Format("2006-01-02")) +} + +func (d DatesFirestoreRepository) GetDates(ctx context.Context, from time.Time, to time.Time) ([]app.Date, error) { + iter := d. + trainerHoursCollection(). + Where("Date", ">=", from). + Where("Date", "<=", to). + Documents(ctx) + + var dates []app.Date + + for { + doc, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + + date := DateModel{} + if err := doc.DataTo(&date); err != nil { + return nil, err + } + dates = append(dates, dateModelToApp(date)) + } + + return dates, nil +} + +func dateModelToApp(dm DateModel) app.Date { + var hours []app.Hour + for _, h := range dm.Hours { + hours = append(hours, app.Hour{ + Available: h.Available, + HasTrainingScheduled: h.HasTrainingScheduled, + Hour: h.Hour, + }) + } + + return app.Date{ + Date: dm.Date, + HasFreeHours: dm.HasFreeHours, + Hours: hours, + } +} + +func (d DatesFirestoreRepository) CanLoadFixtures(ctx context.Context, daysToSet int) (bool, error) { + documents, err := d.trainerHoursCollection().Limit(daysToSet).Documents(ctx).GetAll() + if err != nil { + return false, err + } + + return len(documents) < daysToSet, nil +} diff --git a/internal/trainer/hour_firestore_repository.go b/internal/trainer/adapters/hour_firestore_repository.go similarity index 99% rename from internal/trainer/hour_firestore_repository.go rename to internal/trainer/adapters/hour_firestore_repository.go index e734164..89b3e12 100644 --- a/internal/trainer/hour_firestore_repository.go +++ b/internal/trainer/adapters/hour_firestore_repository.go @@ -1,4 +1,4 @@ -package main +package adapters import ( "context" diff --git a/internal/trainer/hour_memory_repository.go b/internal/trainer/adapters/hour_memory_repository.go similarity index 98% rename from internal/trainer/hour_memory_repository.go rename to internal/trainer/adapters/hour_memory_repository.go index 396f1cf..9ea5eaf 100644 --- a/internal/trainer/hour_memory_repository.go +++ b/internal/trainer/adapters/hour_memory_repository.go @@ -1,4 +1,4 @@ -package main +package adapters import ( "context" diff --git a/internal/trainer/hour_mysql_repository.go b/internal/trainer/adapters/hour_mysql_repository.go similarity index 99% rename from internal/trainer/hour_mysql_repository.go rename to internal/trainer/adapters/hour_mysql_repository.go index 49967fb..c1a316c 100644 --- a/internal/trainer/hour_mysql_repository.go +++ b/internal/trainer/adapters/hour_mysql_repository.go @@ -1,4 +1,4 @@ -package main +package adapters import ( "context" diff --git a/internal/trainer/hour_repository_test.go b/internal/trainer/adapters/hour_repository_test.go similarity index 94% rename from internal/trainer/hour_repository_test.go rename to internal/trainer/adapters/hour_repository_test.go index 847e3e5..c5c9b70 100644 --- a/internal/trainer/hour_repository_test.go +++ b/internal/trainer/adapters/hour_repository_test.go @@ -1,4 +1,4 @@ -package main_test +package adapters_test import ( "context" @@ -9,8 +9,9 @@ import ( "testing" "time" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/adapters" + "cloud.google.com/go/firestore" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -69,7 +70,7 @@ func createRepositories(t *testing.T) []Repository { }, { Name: "memory", - Repository: main.NewMemoryHourRepository(testHourFactory), + Repository: adapters.NewMemoryHourRepository(testHourFactory), }, } } @@ -126,7 +127,7 @@ func testUpdateHour(t *testing.T, repository hour.Repository) { } func testUpdateHour_parallel(t *testing.T, repository hour.Repository) { - if _, ok := repository.(*main.FirestoreHourRepository); ok { + if _, ok := repository.(*adapters.FirestoreHourRepository); ok { // todo - enable after fix of https://github.com/googleapis/google-cloud-go/issues/2604 t.Skip("because of emulator bug, it's not working in Firebase") } @@ -272,7 +273,7 @@ func TestNewDateDTO(t *testing.T) { for _, c := range testCases { t.Run(c.Time.String(), func(t *testing.T) { - dateDTO := main.NewEmptyDateDTO(c.Time) + dateDTO := adapters.NewEmptyDateDTO(c.Time) assert.True(t, dateDTO.Date.Equal(c.ExpectedDateTime), "%s != %s", dateDTO.Date, c.ExpectedDateTime) }) } @@ -288,18 +289,18 @@ var testHourFactory = hour.MustNewFactory(hour.FactoryConfig{ MaxUtcHour: 24, }) -func newFirebaseRepository(t *testing.T, ctx context.Context) *main.FirestoreHourRepository { +func newFirebaseRepository(t *testing.T, ctx context.Context) *adapters.FirestoreHourRepository { firebaseClient, err := firestore.NewClient(ctx, os.Getenv("GCP_PROJECT")) require.NoError(t, err) - return main.NewFirestoreHourRepository(firebaseClient, testHourFactory) + return adapters.NewFirestoreHourRepository(firebaseClient, testHourFactory) } -func newMySQLRepository(t *testing.T) *main.MySQLHourRepository { - db, err := main.NewMySQLConnection() +func newMySQLRepository(t *testing.T) *adapters.MySQLHourRepository { + db, err := adapters.NewMySQLConnection() require.NoError(t, err) - return main.NewMySQLHourRepository(db, testHourFactory) + return adapters.NewMySQLHourRepository(db, testHourFactory) } func newValidAvailableHour(t *testing.T) *hour.Hour { diff --git a/internal/trainer/model.go b/internal/trainer/app/date.go similarity index 51% rename from internal/trainer/model.go rename to internal/trainer/app/date.go index d2f3243..c1b27af 100644 --- a/internal/trainer/model.go +++ b/internal/trainer/app/date.go @@ -1,16 +1,43 @@ -package main +package app import ( "time" ) +type Date struct { + Date time.Time + HasFreeHours bool + Hours []Hour +} + +type Hour struct { + Available bool + HasTrainingScheduled bool + Hour time.Time +} + +func (d Date) FindHourInDate(timeToCheck time.Time) (*Hour, bool) { + for i, hour := range d.Hours { + if hour.Hour == timeToCheck { + return &d.Hours[i], true + } + } + + return nil, false +} + +type AvailableHoursRequest struct { + DateFrom time.Time + DateTo time.Time +} + const ( minHour = 12 maxHour = 20 ) // setDefaultAvailability adds missing hours to Date model if they were not set -func setDefaultAvailability(date DateModel) DateModel { +func setDefaultAvailability(date Date) Date { HoursLoop: for hour := minHour; hour <= maxHour; hour++ { @@ -21,7 +48,7 @@ HoursLoop: continue HoursLoop } } - newHour := HourModel{ + newHour := Hour{ Available: false, Hour: hour, } @@ -32,8 +59,8 @@ HoursLoop: return date } -func addMissingDates(params *GetTrainerAvailableHoursParams, dates []DateModel) []DateModel { - for day := params.DateFrom.UTC(); day.Before(params.DateTo) || day.Equal(params.DateTo); day = day.Add(time.Hour * 24) { +func addMissingDates(dates []Date, from time.Time, to time.Time) []Date { + for day := from.UTC(); day.Before(to) || day.Equal(to); day = day.Add(time.Hour * 24) { found := false for _, date := range dates { if date.Date.Equal(day) { @@ -43,7 +70,7 @@ func addMissingDates(params *GetTrainerAvailableHoursParams, dates []DateModel) } if !found { - date := DateModel{ + date := Date{ Date: day, } date = setDefaultAvailability(date) diff --git a/internal/trainer/app/hour_service.go b/internal/trainer/app/hour_service.go new file mode 100644 index 0000000..7b28e1a --- /dev/null +++ b/internal/trainer/app/hour_service.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "sort" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" +) + +type HourService struct { + datesRepo datesRepository + hourRepo hour.Repository +} + +type datesRepository interface { + GetDates(ctx context.Context, from time.Time, to time.Time) ([]Date, error) +} + +func NewHourService(datesRepo datesRepository, hourRepo hour.Repository) HourService { + return HourService{ + datesRepo: datesRepo, + hourRepo: hourRepo, + } +} + +func (c HourService) GetTrainerAvailableHours(ctx context.Context, from time.Time, to time.Time) ([]Date, error) { + if from.After(to) { + return nil, errors.NewIncorrectInputError("date-from-after-date-to", "Date from after date to") + } + + dates, err := c.datesRepo.GetDates(ctx, from, to) + if err != nil { + return nil, err + } + + dates = addMissingDates(dates, from, to) + + for i, date := range dates { + date = setDefaultAvailability(date) + sort.Slice(date.Hours, func(i, j int) bool { return date.Hours[i].Hour.Before(date.Hours[j].Hour) }) + dates[i] = date + } + sort.Slice(dates, func(i, j int) bool { return dates[i].Date.Before(dates[j].Date) }) + + return dates, nil +} + +func (c HourService) MakeHoursAvailable(ctx context.Context, hours []time.Time) error { + for _, hourToUpdate := range hours { + if err := c.hourRepo.UpdateHour(ctx, hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeAvailable(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + } + + return nil +} + +func (c HourService) MakeHoursUnavailable(ctx context.Context, hours []time.Time) error { + for _, hourToUpdate := range hours { + if err := c.hourRepo.UpdateHour(ctx, hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeNotAvailable(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + } + + return nil +} diff --git a/internal/trainer/firestore.go b/internal/trainer/firestore.go deleted file mode 100644 index 280ce3e..0000000 --- a/internal/trainer/firestore.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "context" - "sort" - "time" - - "cloud.google.com/go/firestore" - "google.golang.org/api/iterator" -) - -type DateModel struct { - Date time.Time `firestore:"Date"` - HasFreeHours bool `firestore:"HasFreeHours"` - Hours []HourModel `firestore:"Hours"` -} - -type HourModel struct { - Available bool `firestore:"Available"` - HasTrainingScheduled bool `firestore:"HasTrainingScheduled"` - Hour time.Time `firestore:"Hour"` -} - -type db struct { - firestoreClient *firestore.Client -} - -func (d db) TrainerHoursCollection() *firestore.CollectionRef { - return d.firestoreClient.Collection("trainer-hours") -} - -func (d db) DocumentRef(dateTimeToUpdate time.Time) *firestore.DocumentRef { - return d.TrainerHoursCollection().Doc(dateTimeToUpdate.Format("2006-01-02")) -} - -func (d db) GetDates(ctx context.Context, params *GetTrainerAvailableHoursParams) ([]DateModel, error) { - dates, err := d.QueryDates(params, ctx) - if err != nil { - return nil, err - } - dates = addMissingDates(params, dates) - - for _, date := range dates { - sort.Slice(date.Hours, func(i, j int) bool { return date.Hours[i].Hour.Before(date.Hours[j].Hour) }) - } - sort.Slice(dates, func(i, j int) bool { return dates[i].Date.Before(dates[j].Date) }) - - return dates, nil -} - -func (d db) QueryDates(params *GetTrainerAvailableHoursParams, ctx context.Context) ([]DateModel, error) { - iter := d. - TrainerHoursCollection(). - Where("Date", ">=", params.DateFrom). - Where("Date", "<=", params.DateTo). - Documents(ctx) - - var dates []DateModel - - for { - doc, err := iter.Next() - if err == iterator.Done { - break - } - if err != nil { - return nil, err - } - - date := DateModel{} - if err := doc.DataTo(&date); err != nil { - return nil, err - } - date = setDefaultAvailability(date) - dates = append(dates, date) - } - - return dates, nil -} diff --git a/internal/trainer/fixtures.go b/internal/trainer/fixtures.go index fe8035f..a92169e 100644 --- a/internal/trainer/fixtures.go +++ b/internal/trainer/fixtures.go @@ -12,7 +12,11 @@ import ( "github.com/sirupsen/logrus" ) -func loadFixtures(db db) { +type fixturesChecker interface { + CanLoadFixtures(ctx context.Context, daysToSet int) (bool, error) +} + +func loadFixtures(checker fixturesChecker) { start := time.Now() ctx := context.Background() @@ -29,7 +33,7 @@ func loadFixtures(db db) { var err error for { - canLoad, err = canLoadFixtures(ctx, db) + canLoad, err = checker.CanLoadFixtures(ctx, daysToSet) if err == nil { break } @@ -94,12 +98,3 @@ func loadTrainerFixtures(ctx context.Context) error { return nil } - -func canLoadFixtures(ctx context.Context, db db) (bool, error) { - documents, err := db.TrainerHoursCollection().Limit(daysToSet).Documents(ctx).GetAll() - if err != nil { - return false, err - } - - return len(documents) < daysToSet, nil -} diff --git a/internal/trainer/main.go b/internal/trainer/main.go index 99a0055..01dcd83 100644 --- a/internal/trainer/main.go +++ b/internal/trainer/main.go @@ -11,7 +11,10 @@ import ( "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/adapters" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/ports" "github.com/go-chi/chi" "google.golang.org/grpc" ) @@ -25,7 +28,7 @@ func main() { panic(err) } - firebaseDB := db{firestoreClient} + datesRepository := adapters.NewDatesFirestoreRepository(firestoreClient) hourFactory, err := hour.NewFactory(hour.FactoryConfig{ MaxWeeksInTheFutureToSet: 6, @@ -36,23 +39,24 @@ func main() { panic(err) } + hourRepository := adapters.NewFirestoreHourRepository(firestoreClient, hourFactory) + + service := app.NewHourService(datesRepository, hourRepository) + serverType := strings.ToLower(os.Getenv("SERVER_TO_RUN")) switch serverType { case "http": - go loadFixtures(firebaseDB) + go loadFixtures(datesRepository) server.RunHTTPServer(func(router chi.Router) http.Handler { - return HandlerFromMux( - HttpServer{ - firebaseDB, - NewFirestoreHourRepository(firestoreClient, hourFactory), - }, + return ports.HandlerFromMux( + ports.NewHttpServer(service), router, ) }) case "grpc": server.RunGRPCServer(func(server *grpc.Server) { - svc := GrpcServer{NewFirestoreHourRepository(firestoreClient, hourFactory)} + svc := ports.NewGrpcServer(hourRepository) trainer.RegisterTrainerServiceServer(server, svc) }) default: diff --git a/internal/trainer/grpc.go b/internal/trainer/ports/grpc.go similarity index 93% rename from internal/trainer/grpc.go rename to internal/trainer/ports/grpc.go index 7a7da25..d2cdbeb 100644 --- a/internal/trainer/grpc.go +++ b/internal/trainer/ports/grpc.go @@ -1,4 +1,4 @@ -package main +package ports import ( "context" @@ -17,6 +17,16 @@ type GrpcServer struct { hourRepository hour.Repository } +func NewGrpcServer(hourRepository hour.Repository) GrpcServer { + if hourRepository == nil { + panic("missing hourRepository") + } + + return GrpcServer{ + hourRepository: hourRepository, + } +} + func (g GrpcServer) MakeHourAvailable(ctx context.Context, request *trainer.UpdateHourRequest) (*trainer.EmptyResponse, error) { trainingTime, err := protoTimestampToTime(request.Time) if err != nil { diff --git a/internal/trainer/http.go b/internal/trainer/ports/http.go similarity index 59% rename from internal/trainer/http.go rename to internal/trainer/ports/http.go index f52c6bc..5000f0a 100644 --- a/internal/trainer/http.go +++ b/internal/trainer/ports/http.go @@ -1,32 +1,31 @@ -package main +package ports import ( "net/http" - openapi_types "github.com/deepmap/oapi-codegen/pkg/types" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + openapi_types "github.com/deepmap/oapi-codegen/pkg/types" "github.com/go-chi/render" ) type HttpServer struct { - db db - hourRepository hour.Repository + service app.HourService +} + +func NewHttpServer(service app.HourService) HttpServer { + return HttpServer{ + service: service, + } } func (h HttpServer) GetTrainerAvailableHours(w http.ResponseWriter, r *http.Request) { queryParams := r.Context().Value("GetTrainerAvailableHoursParams").(*GetTrainerAvailableHoursParams) - if queryParams.DateFrom.After(queryParams.DateTo) { - httperr.BadRequest("date-from-after-date-to", nil, w, r) - return - } - - dateModels, err := h.db.GetDates(r.Context(), queryParams) + dateModels, err := h.service.GetTrainerAvailableHours(r.Context(), queryParams.DateFrom, queryParams.DateTo) if err != nil { - httperr.InternalError("unable-to-get-dates", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } @@ -34,7 +33,7 @@ func (h HttpServer) GetTrainerAvailableHours(w http.ResponseWriter, r *http.Requ render.Respond(w, r, dates) } -func dateModelsToResponse(models []DateModel) []Date { +func dateModelsToResponse(models []app.Date) []Date { var dates []Date for _, d := range models { var hours []Hour @@ -61,7 +60,7 @@ func dateModelsToResponse(models []DateModel) []Date { func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } @@ -72,20 +71,14 @@ func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { hourUpdate := &HourUpdate{} if err := render.Decode(r, hourUpdate); err != nil { - httperr.BadRequest("unable-to-update-availability", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - for _, hourToUpdate := range hourUpdate.Hours { - if err := h.hourRepository.UpdateHour(r.Context(), hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { - if err := h.MakeAvailable(); err != nil { - return nil, err - } - return h, nil - }); err != nil { - httperr.InternalError("unable-to-update-availability", err, w, r) - return - } + err = h.service.MakeHoursAvailable(r.Context(), hourUpdate.Hours) + if err != nil { + httperr.RespondWithSlugError(err, w, r) + return } w.WriteHeader(http.StatusNoContent) @@ -94,9 +87,10 @@ func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { func (h HttpServer) MakeHourUnavailable(w http.ResponseWriter, r *http.Request) { user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } + if user.Role != "trainer" { httperr.Unauthorised("invalid-role", nil, w, r) return @@ -104,20 +98,14 @@ func (h HttpServer) MakeHourUnavailable(w http.ResponseWriter, r *http.Request) hourUpdate := &HourUpdate{} if err := render.Decode(r, hourUpdate); err != nil { - httperr.BadRequest("unable-to-update-availability", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - for _, hourToUpdate := range hourUpdate.Hours { - if err := h.hourRepository.UpdateHour(r.Context(), hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { - if err := h.MakeNotAvailable(); err != nil { - return nil, err - } - return h, nil - }); err != nil { - httperr.InternalError("unable-to-update-availability", err, w, r) - return - } + err = h.service.MakeHoursUnavailable(r.Context(), hourUpdate.Hours) + if err != nil { + httperr.RespondWithSlugError(err, w, r) + return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/trainer/openapi_api.gen.go b/internal/trainer/ports/openapi_api.gen.go similarity index 97% rename from internal/trainer/openapi_api.gen.go rename to internal/trainer/ports/openapi_api.gen.go index ddc9473..2fdb33d 100644 --- a/internal/trainer/openapi_api.gen.go +++ b/internal/trainer/ports/openapi_api.gen.go @@ -1,14 +1,15 @@ -// Package main provides primitives to interact the openapi HTTP API. +// Package ports provides primitives to interact the openapi HTTP API. // // Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. -package main +package ports import ( "context" "fmt" + "net/http" + "github.com/deepmap/oapi-codegen/pkg/runtime" "github.com/go-chi/chi" - "net/http" ) type ServerInterface interface { diff --git a/internal/trainer/openapi_types.gen.go b/internal/trainer/ports/openapi_types.gen.go similarity index 94% rename from internal/trainer/openapi_types.gen.go rename to internal/trainer/ports/openapi_types.gen.go index 21e2b4b..17fb787 100644 --- a/internal/trainer/openapi_types.gen.go +++ b/internal/trainer/ports/openapi_types.gen.go @@ -1,11 +1,12 @@ -// Package main provides primitives to interact the openapi HTTP API. +// Package ports provides primitives to interact the openapi HTTP API. // // Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. -package main +package ports import ( - openapi_types "github.com/deepmap/oapi-codegen/pkg/types" "time" + + openapi_types "github.com/deepmap/oapi-codegen/pkg/types" ) // Date defines model for Date. diff --git a/internal/trainings/adapters/trainer_grpc.go b/internal/trainings/adapters/trainer_grpc.go new file mode 100644 index 0000000..5644650 --- /dev/null +++ b/internal/trainings/adapters/trainer_grpc.go @@ -0,0 +1,45 @@ +package adapters + +import ( + "context" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/pkg/errors" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" +) + +type TrainerGrpc struct { + client trainer.TrainerServiceClient +} + +func NewTrainerGrpc(client trainer.TrainerServiceClient) TrainerGrpc { + return TrainerGrpc{client: client} +} + +func (s TrainerGrpc) ScheduleTraining(ctx context.Context, trainingTime time.Time) error { + timestamp, err := ptypes.TimestampProto(trainingTime) + if err != nil { + return errors.Wrap(err, "unable to convert time to proto timestamp") + } + + _, err = s.client.ScheduleTraining(ctx, &trainer.UpdateHourRequest{ + Time: timestamp, + }) + + return err +} + +func (s TrainerGrpc) CancelTraining(ctx context.Context, trainingTime time.Time) error { + timestamp, err := ptypes.TimestampProto(trainingTime) + if err != nil { + return errors.Wrap(err, "unable to convert time to proto timestamp") + } + + _, err = s.client.CancelTraining(ctx, &trainer.UpdateHourRequest{ + Time: timestamp, + }) + + return err +} diff --git a/internal/trainings/adapters/trainings_firestore_repository.go b/internal/trainings/adapters/trainings_firestore_repository.go new file mode 100644 index 0000000..7d994d0 --- /dev/null +++ b/internal/trainings/adapters/trainings_firestore_repository.go @@ -0,0 +1,218 @@ +package adapters + +import ( + "context" + "sort" + "time" + + "cloud.google.com/go/firestore" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app" + "github.com/pkg/errors" + "google.golang.org/api/iterator" +) + +type TrainingModel struct { + UUID string `firestore:"Uuid"` + UserUUID string `firestore:"UserUuid"` + User string `firestore:"User"` + + Time time.Time `firestore:"Time"` + Notes string `firestore:"Notes"` + + ProposedTime *time.Time `firestore:"ProposedTime"` + MoveProposedBy *string `firestore:"MoveProposedBy"` +} + +func (t TrainingModel) canBeCancelled() bool { + return t.Time.Sub(time.Now()) > time.Hour*24 +} + +type TrainingsFirestoreRepository struct { + firestoreClient *firestore.Client +} + +func NewTrainingsFirestoreRepository( + firestoreClient *firestore.Client, +) TrainingsFirestoreRepository { + return TrainingsFirestoreRepository{ + firestoreClient: firestoreClient, + } +} + +func (d TrainingsFirestoreRepository) trainingsCollection() *firestore.CollectionRef { + return d.firestoreClient.Collection("trainings") +} + +func (d TrainingsFirestoreRepository) AllTrainings(ctx context.Context) ([]app.Training, error) { + query := d.trainingsCollection().Query.Where("Time", ">=", time.Now().Add(-time.Hour*24)) + + iter := query.Documents(ctx) + + return trainingModelsToApp(iter) +} + +func (d TrainingsFirestoreRepository) FindTrainingsForUser(ctx context.Context, user auth.User) ([]app.Training, error) { + query := d.trainingsCollection().Query. + Where("Time", ">=", time.Now().Add(-time.Hour*24)). + Where("UserUuid", "==", user.UUID) + + iter := query.Documents(ctx) + + return trainingModelsToApp(iter) +} + +func trainingModelsToApp(iter *firestore.DocumentIterator) ([]app.Training, error) { + var trainings []app.Training + + for { + doc, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + + t := TrainingModel{} + if err := doc.DataTo(&t); err != nil { + return nil, err + } + + trainings = append(trainings, app.Training(t)) + } + + sort.Slice(trainings, func(i, j int) bool { return trainings[i].Time.Before(trainings[j].Time) }) + + return trainings, nil +} + +func (d TrainingsFirestoreRepository) CreateTraining(ctx context.Context, training app.Training, createFn func() error) error { + collection := d.trainingsCollection() + + trainingModel := TrainingModel(training) + + return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { + docs, err := tx.Documents(collection.Where("Time", "==", trainingModel.Time)).GetAll() + if err != nil { + return errors.Wrap(err, "unable to get actual docs") + } + if len(docs) > 0 { + return errors.Errorf("there is training already at %s", trainingModel.Time) + } + + err = createFn() + if err != nil { + return err + } + + return tx.Create(collection.Doc(trainingModel.UUID), trainingModel) + }) +} + +func (d TrainingsFirestoreRepository) CancelTraining(ctx context.Context, trainingUUID string, deleteFn func(app.Training) error) error { + trainingsCollection := d.trainingsCollection() + + return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { + trainingDocumentRef := trainingsCollection.Doc(trainingUUID) + + firestoreTraining, err := tx.Get(trainingDocumentRef) + if err != nil { + return errors.Wrap(err, "unable to get actual docs") + } + + training := TrainingModel{} + err = firestoreTraining.DataTo(&training) + if err != nil { + return errors.Wrap(err, "unable to load document") + } + + err = deleteFn(app.Training(training)) + if err != nil { + return err + } + + return tx.Delete(trainingDocumentRef) + }) +} + +func (d TrainingsFirestoreRepository) RescheduleTraining( + ctx context.Context, + trainingUUID string, + newTime time.Time, + updateFn func(app.Training) (app.Training, error), +) error { + collection := d.trainingsCollection() + + return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { + doc, err := tx.Get(d.trainingsCollection().Doc(trainingUUID)) + if err != nil { + return errors.Wrap(err, "could not find training") + } + + docs, err := tx.Documents(collection.Where("Time", "==", newTime)).GetAll() + if err != nil { + return errors.Wrap(err, "unable to get actual docs") + } + if len(docs) > 0 { + return errors.Errorf("there is training already at %s", newTime) + } + + var training TrainingModel + err = doc.DataTo(&training) + if err != nil { + return errors.Wrap(err, "could not unmarshal training") + } + + updatedTraining, err := updateFn(app.Training(training)) + if err != nil { + return err + } + + return tx.Set(collection.Doc(training.UUID), TrainingModel(updatedTraining)) + }) +} + +func (d TrainingsFirestoreRepository) ApproveTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(app.Training) (app.Training, error)) error { + return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { + doc, err := tx.Get(d.trainingsCollection().Doc(trainingUUID)) + if err != nil { + return errors.Wrap(err, "could not find training") + } + + var training TrainingModel + err = doc.DataTo(&training) + if err != nil { + return errors.Wrap(err, "could not unmarshal training") + } + + updatedTraining, err := updateFn(app.Training(training)) + if err != nil { + return err + } + + return tx.Set(d.trainingsCollection().Doc(training.UUID), TrainingModel(updatedTraining)) + }) +} + +func (d TrainingsFirestoreRepository) RejectTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(app.Training) (app.Training, error)) error { + return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { + doc, err := tx.Get(d.trainingsCollection().Doc(trainingUUID)) + if err != nil { + return errors.Wrap(err, "could not find training") + } + + var training TrainingModel + err = doc.DataTo(&training) + if err != nil { + return errors.Wrap(err, "could not unmarshal training") + } + + updatedTraining, err := updateFn(app.Training(training)) + if err != nil { + return err + } + + return tx.Set(d.trainingsCollection().Doc(training.UUID), TrainingModel(updatedTraining)) + }) +} diff --git a/internal/trainings/adapters/users_grpc.go b/internal/trainings/adapters/users_grpc.go new file mode 100644 index 0000000..49a7ac0 --- /dev/null +++ b/internal/trainings/adapters/users_grpc.go @@ -0,0 +1,24 @@ +package adapters + +import ( + "context" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/users" +) + +type UsersGrpc struct { + client users.UsersServiceClient +} + +func NewUsersGrpc(client users.UsersServiceClient) UsersGrpc { + return UsersGrpc{client: client} +} + +func (s UsersGrpc) UpdateTrainingBalance(ctx context.Context, userID string, amountChange int) error { + _, err := s.client.UpdateTrainingBalance(ctx, &users.UpdateTrainingBalanceRequest{ + UserId: userID, + AmountChange: int64(amountChange), + }) + + return err +} diff --git a/internal/trainings/app/training.go b/internal/trainings/app/training.go new file mode 100644 index 0000000..dd093f5 --- /dev/null +++ b/internal/trainings/app/training.go @@ -0,0 +1,23 @@ +package app + +import "time" + +type Training struct { + UUID string + UserUUID string + User string + + Time time.Time + Notes string + + ProposedTime *time.Time + MoveProposedBy *string +} + +func (t Training) CanBeCancelled() bool { + return t.Time.Sub(time.Now()) > time.Hour*24 +} + +func (t Training) MoveRequiresAccept() bool { + return !t.CanBeCancelled() +} diff --git a/internal/trainings/app/training_service.go b/internal/trainings/app/training_service.go new file mode 100644 index 0000000..8971864 --- /dev/null +++ b/internal/trainings/app/training_service.go @@ -0,0 +1,198 @@ +package app + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" + commonerrors "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/google/uuid" + "github.com/pkg/errors" +) + +type trainingRepository interface { + FindTrainingsForUser(ctx context.Context, user auth.User) ([]Training, error) + AllTrainings(ctx context.Context) ([]Training, error) + CreateTraining(ctx context.Context, training Training, createFn func() error) error + CancelTraining(ctx context.Context, trainingUUID string, deleteFn func(Training) error) error + RescheduleTraining(ctx context.Context, trainingUUID string, newTime time.Time, updateFn func(Training) (Training, error)) error + ApproveTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(Training) (Training, error)) error + RejectTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(Training) (Training, error)) error +} + +type userService interface { + UpdateTrainingBalance(ctx context.Context, userID string, amountChange int) error +} + +type trainerService interface { + ScheduleTraining(ctx context.Context, trainingTime time.Time) error + CancelTraining(ctx context.Context, trainingTime time.Time) error +} + +type TrainingService struct { + repo trainingRepository + trainerService trainerService + userService userService +} + +func NewTrainingsService( + repo trainingRepository, + trainerService trainerService, + userService userService, +) TrainingService { + if repo == nil { + panic("missing trainingRepository") + } + if trainerService == nil { + panic("missing trainerService") + } + if userService == nil { + panic("missing userService") + } + + return TrainingService{ + repo: repo, + trainerService: trainerService, + userService: userService, + } +} + +func (c TrainingService) GetAllTrainings(ctx context.Context) ([]Training, error) { + return c.repo.AllTrainings(ctx) +} + +func (c TrainingService) GetTrainingsForUser(ctx context.Context, user auth.User) ([]Training, error) { + return c.repo.FindTrainingsForUser(ctx, user) +} + +func (c TrainingService) CreateTraining(ctx context.Context, user auth.User, trainingTime time.Time, notes string) error { + // sanity check + if len(notes) > 1000 { + return commonerrors.NewIncorrectInputError("Note too big", "note-too-big") + } + + training := Training{ + UUID: uuid.New().String(), + UserUUID: user.UUID, + User: user.DisplayName, + Notes: notes, + Time: trainingTime, + } + + return c.repo.CreateTraining(ctx, training, func() error { + err := c.userService.UpdateTrainingBalance(ctx, user.UUID, -1) + if err != nil { + return errors.Wrap(err, "unable to change trainings balance") + } + + err = c.trainerService.ScheduleTraining(ctx, training.Time) + if err != nil { + return errors.Wrap(err, "unable to schedule training") + } + + return nil + }) +} + +func (c TrainingService) RescheduleTraining(ctx context.Context, user auth.User, trainingUUID string, newTime time.Time, newNotes string) error { + // sanity check + if len(newNotes) > 1000 { + return commonerrors.NewIncorrectInputError("Note too big", "note-too-big") + } + + return c.repo.RescheduleTraining(ctx, trainingUUID, newTime, func(training Training) (Training, error) { + if training.CanBeCancelled() { + err := c.trainerService.ScheduleTraining(ctx, newTime) + if err != nil { + return Training{}, errors.Wrap(err, "unable to schedule training") + } + + err = c.trainerService.CancelTraining(ctx, training.Time) + if err != nil { + return Training{}, errors.Wrap(err, "unable to cancel training") + } + + training.Time = newTime + training.Notes = newNotes + } else { + training.ProposedTime = &newTime + training.MoveProposedBy = &user.Role + training.Notes = newNotes + } + + return training, nil + }) +} + +func (c TrainingService) ApproveTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { + return c.repo.ApproveTrainingReschedule(ctx, trainingUUID, func(training Training) (Training, error) { + if training.ProposedTime == nil { + return Training{}, errors.New("training has no proposed time") + } + if training.MoveProposedBy == nil { + return Training{}, errors.New("training has no MoveProposedBy") + } + if *training.MoveProposedBy == "trainer" && training.UserUUID != user.UUID { + return Training{}, errors.Errorf("user '%s' cannot approve reschedule of user '%s'", user.UUID, training.UserUUID) + } + if *training.MoveProposedBy == user.Role { + return Training{}, errors.New("reschedule cannot be accepted by requesting person") + } + + training.Time = *training.ProposedTime + training.ProposedTime = nil + + return training, nil + }) +} + +func (c TrainingService) RejectTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { + return c.repo.RejectTrainingReschedule(ctx, trainingUUID, func(training Training) (Training, error) { + if training.MoveProposedBy == nil { + return Training{}, errors.New("training has no MoveProposedBy") + } + if *training.MoveProposedBy != "trainer" && training.UserUUID != user.UUID { + return Training{}, errors.Errorf("user '%s' cannot approve reschedule of user '%s'", user.UUID, training.UserUUID) + } + + training.ProposedTime = nil + + return training, nil + }) +} + +func (c TrainingService) CancelTraining(ctx context.Context, user auth.User, trainingUUID string) error { + return c.repo.CancelTraining(ctx, trainingUUID, func(training Training) error { + if user.Role != "trainer" && training.UserUUID != user.UUID { + return errors.Errorf("user '%s' is trying to cancel training of user '%s'", user.UUID, training.UserUUID) + } + + var trainingBalanceDelta int + if training.CanBeCancelled() { + // just give training back + trainingBalanceDelta = 1 + } else { + if user.Role == "trainer" { + // 1 for cancelled training +1 fine for cancelling by trainer less than 24h before training + trainingBalanceDelta = 2 + } else { + // fine for cancelling less than 24h before training + trainingBalanceDelta = 0 + } + } + + if trainingBalanceDelta != 0 { + err := c.userService.UpdateTrainingBalance(ctx, training.UserUUID, trainingBalanceDelta) + if err != nil { + return errors.Wrap(err, "unable to change trainings balance") + } + } + + err := c.trainerService.CancelTraining(ctx, training.Time) + if err != nil { + return errors.Wrap(err, "unable to cancel training") + } + + return nil + }) +} diff --git a/internal/trainings/app/training_service_test.go b/internal/trainings/app/training_service_test.go new file mode 100644 index 0000000..fc0cc0c --- /dev/null +++ b/internal/trainings/app/training_service_test.go @@ -0,0 +1,194 @@ +package app_test + +import ( + "context" + "testing" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app" + "github.com/stretchr/testify/require" +) + +func TestCancelTraining(t *testing.T) { + requestingUserID := "requesting-user-id" + + testCases := []struct { + Name string + UserRole string + + Training app.Training + + ShouldFail bool + ExpectedError string + + ShouldUpdateBalance bool + ExpectedBalanceChange int + }{ + { + Name: "return_training_balance_when_attendee_cancels", + UserRole: "attendee", + Training: app.Training{ + UserUUID: requestingUserID, + Time: time.Now().Add(48 * time.Hour), + }, + ShouldUpdateBalance: true, + ExpectedBalanceChange: 1, + }, + { + Name: "return_training_balance_when_trainer_cancels", + UserRole: "trainer", + Training: app.Training{ + UserUUID: "trainer-id", + Time: time.Now().Add(48 * time.Hour), + }, + ShouldUpdateBalance: true, + ExpectedBalanceChange: 1, + }, + { + Name: "extra_training_balance_when_trainer_cancels_before_24h", + UserRole: "trainer", + Training: app.Training{ + UserUUID: "trainer-id", + Time: time.Now().Add(12 * time.Hour), + }, + ShouldUpdateBalance: true, + ExpectedBalanceChange: 2, + }, + { + Name: "no_training_balance_returned_when_attendee_cancels_before_24h", + UserRole: "attendee", + Training: app.Training{ + UserUUID: requestingUserID, + Time: time.Now().Add(12 * time.Hour), + }, + ShouldUpdateBalance: false, + }, + { + Name: "fail_updating_other_attendee_training", + UserRole: "attendee", + Training: app.Training{ + UserUUID: "another-attendee-id", + Time: time.Now().Add(48 * time.Hour), + }, + ShouldFail: true, + ExpectedError: "user 'requesting-user-id' is trying to cancel training of user 'another-attendee-id'", + ShouldUpdateBalance: false, + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + trainingUUID := "any-training-uuid" + deps := newDependencies() + deps.repository.training = tc.Training + + user := auth.User{ + UUID: requestingUserID, + Role: tc.UserRole, + } + + err := deps.trainingsService.CancelTraining(context.Background(), user, trainingUUID) + + if tc.ShouldFail { + require.EqualError(t, err, tc.ExpectedError) + return + } + + require.NoError(t, err) + + if tc.ShouldUpdateBalance { + require.Len(t, deps.userService.balanceUpdates, 1) + require.Equal(t, tc.Training.UserUUID, deps.userService.balanceUpdates[0].userID) + require.Equal(t, tc.ExpectedBalanceChange, deps.userService.balanceUpdates[0].amountChange) + } else { + require.Len(t, deps.userService.balanceUpdates, 0) + } + + require.Len(t, deps.trainerService.trainingsCancelled, 1) + require.Equal(t, tc.Training.Time, deps.trainerService.trainingsCancelled[0]) + }) + } +} + +type dependencies struct { + repository *repositoryMock + trainerService *trainerServiceMock + userService *userServiceMock + trainingsService app.TrainingService +} + +func newDependencies() dependencies { + repository := &repositoryMock{} + trainerService := &trainerServiceMock{} + userService := &userServiceMock{} + + return dependencies{ + repository: repository, + trainerService: trainerService, + userService: userService, + trainingsService: app.NewTrainingsService(repository, trainerService, userService), + } +} + +type repositoryMock struct { + training app.Training +} + +func (r repositoryMock) FindTrainingsForUser(ctx context.Context, user auth.User) ([]app.Training, error) { + panic("implement me") +} + +func (r repositoryMock) AllTrainings(ctx context.Context) ([]app.Training, error) { + panic("implement me") +} + +func (r repositoryMock) CreateTraining(ctx context.Context, training app.Training, createFn func() error) error { + panic("implement me") +} + +func (r repositoryMock) CancelTraining(ctx context.Context, trainingUUID string, deleteFn func(app.Training) error) error { + return deleteFn(r.training) +} + +func (r repositoryMock) RescheduleTraining(ctx context.Context, trainingUUID string, newTime time.Time, updateFn func(app.Training) (app.Training, error)) error { + panic("implement me") +} + +func (r repositoryMock) ApproveTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(app.Training) (app.Training, error)) error { + panic("implement me") +} + +func (r repositoryMock) RejectTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(app.Training) (app.Training, error)) error { + panic("implement me") +} + +type trainerServiceMock struct { + trainingsCancelled []time.Time +} + +func (t *trainerServiceMock) ScheduleTraining(ctx context.Context, trainingTime time.Time) error { + panic("implement me") +} + +func (t *trainerServiceMock) CancelTraining(ctx context.Context, trainingTime time.Time) error { + t.trainingsCancelled = append(t.trainingsCancelled, trainingTime) + return nil +} + +type balanceUpdate struct { + userID string + amountChange int +} + +type userServiceMock struct { + balanceUpdates []balanceUpdate +} + +func (u *userServiceMock) UpdateTrainingBalance(ctx context.Context, userID string, amountChange int) error { + u.balanceUpdates = append(u.balanceUpdates, balanceUpdate{userID, amountChange}) + return nil +} diff --git a/internal/trainings/firestore.go b/internal/trainings/firestore.go deleted file mode 100644 index 5d836cb..0000000 --- a/internal/trainings/firestore.go +++ /dev/null @@ -1,296 +0,0 @@ -package main - -import ( - "context" - "sort" - "time" - - "cloud.google.com/go/firestore" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" - "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/users" - "github.com/golang/protobuf/ptypes" - "github.com/pkg/errors" - "google.golang.org/api/iterator" -) - -type TrainingModel struct { - UUID string `firestore:"Uuid"` - UserUUID string `firestore:"UserUuid"` - User string `firestore:"User"` - - Time time.Time `firestore:"Time"` - Notes string `firestore:"Notes"` - - ProposedTime *time.Time `firestore:"ProposedTime"` - MoveProposedBy *string `firestore:"MoveProposedBy"` -} - -func (t TrainingModel) canBeCancelled() bool { - return t.Time.Sub(time.Now()) > time.Hour*24 -} - -type db struct { - firestoreClient *firestore.Client - trainerClient trainer.TrainerServiceClient - usersClient users.UsersServiceClient -} - -func (d db) TrainingsCollection() *firestore.CollectionRef { - return d.firestoreClient.Collection("trainings") -} - -func (d db) GetTrainings(ctx context.Context, user auth.User) ([]TrainingModel, error) { - query := d.TrainingsCollection().Query.Where("Time", ">=", time.Now().Add(-time.Hour*24)) - - if user.Role != "trainer" { - query = query.Where("UserUuid", "==", user.UUID) - } - - iter := query.Documents(ctx) - - var trainings []TrainingModel - - for { - doc, err := iter.Next() - if err == iterator.Done { - break - } - if err != nil { - return nil, err - } - - t := TrainingModel{} - if err := doc.DataTo(&t); err != nil { - return nil, err - } - - trainings = append(trainings, t) - } - - sort.Slice(trainings, func(i, j int) bool { return trainings[i].Time.Before(trainings[j].Time) }) - - return trainings, nil -} - -func (d db) CreateTraining(ctx context.Context, user auth.User, training TrainingModel) error { - collection := d.TrainingsCollection() - - return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { - docs, err := tx.Documents(collection.Where("Time", "==", training.Time)).GetAll() - if err != nil { - return errors.Wrap(err, "unable to get actual docs") - } - if len(docs) > 0 { - return errors.Errorf("there is training already at %s", training.Time) - } - - _, err = d.usersClient.UpdateTrainingBalance(ctx, &users.UpdateTrainingBalanceRequest{ - UserId: user.UUID, - AmountChange: -1, - }) - if err != nil { - return errors.Wrap(err, "unable to change trainings balance") - } - - timestamp, err := ptypes.TimestampProto(training.Time) - if err != nil { - return errors.Wrap(err, "unable to convert time to proto timestamp") - } - _, err = d.trainerClient.ScheduleTraining(ctx, &trainer.UpdateHourRequest{ - Time: timestamp, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - return tx.Create(collection.Doc(training.UUID), training) - }) -} - -func (d db) CancelTraining(ctx context.Context, user auth.User, trainingUUID string) error { - trainingsCollection := d.TrainingsCollection() - - return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { - trainingDocumentRef := trainingsCollection.Doc(trainingUUID) - - firestoreTraining, err := tx.Get(trainingDocumentRef) - if err != nil { - return errors.Wrap(err, "unable to get actual docs") - } - - training := &TrainingModel{} - err = firestoreTraining.DataTo(training) - if err != nil { - return errors.Wrap(err, "unable to load document") - } - - if user.Role != "trainer" && training.UserUUID != user.UUID { - return errors.Errorf("user '%s' is trying to cancel training of user '%s'", user.UUID, training.UserUUID) - } - - var trainingBalanceDelta int64 - if training.canBeCancelled() { - // just give training back - trainingBalanceDelta = 1 - } else { - if user.Role == "trainer" { - // 1 for cancelled training +1 fine for cancelling by trainer less than 24h before training - trainingBalanceDelta = 2 - } else { - // fine for cancelling less than 24h before training - trainingBalanceDelta = 0 - } - } - - if trainingBalanceDelta != 0 { - _, err := d.usersClient.UpdateTrainingBalance(ctx, &users.UpdateTrainingBalanceRequest{ - UserId: training.UserUUID, - AmountChange: trainingBalanceDelta, - }) - if err != nil { - return errors.Wrap(err, "unable to change trainings balance") - } - } - - timestamp, err := ptypes.TimestampProto(training.Time) - if err != nil { - return errors.Wrap(err, "unable to convert time to proto timestamp") - } - _, err = d.trainerClient.CancelTraining(ctx, &trainer.UpdateHourRequest{ - Time: timestamp, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - return tx.Delete(trainingDocumentRef) - }) -} - -func (d db) RescheduleTraining(ctx context.Context, user auth.User, trainingUUID string, newTime time.Time, notes string) error { - collection := d.TrainingsCollection() - - return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(d.TrainingsCollection().Doc(trainingUUID)) - if err != nil { - return errors.Wrap(err, "could not find training") - } - - docs, err := tx.Documents(collection.Where("Time", "==", newTime)).GetAll() - if err != nil { - return errors.Wrap(err, "unable to get actual docs") - } - if len(docs) > 0 { - return errors.Errorf("there is training already at %s", newTime) - } - - var training TrainingModel - err = doc.DataTo(&training) - if err != nil { - return errors.Wrap(err, "could not unmarshal training") - } - - if training.canBeCancelled() { - err = d.rescheduleTraining(ctx, training.Time, newTime) - if err != nil { - return errors.Wrap(err, "unable to reschedule training") - } - - training.Time = newTime - training.Notes = notes - } else { - training.ProposedTime = &newTime - training.MoveProposedBy = &user.Role - training.Notes = notes - } - - return tx.Set(collection.Doc(training.UUID), training) - }) -} -func (d db) rescheduleTraining(ctx context.Context, oldTime, newTime time.Time) error { - oldTimeProto, err := ptypes.TimestampProto(oldTime) - if err != nil { - return errors.Wrap(err, "unable to convert time to proto timestamp") - } - - newTimeProto, err := ptypes.TimestampProto(newTime) - if err != nil { - return errors.Wrap(err, "unable to convert time to proto timestamp") - } - - _, err = d.trainerClient.ScheduleTraining(ctx, &trainer.UpdateHourRequest{ - Time: newTimeProto, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - _, err = d.trainerClient.CancelTraining(ctx, &trainer.UpdateHourRequest{ - Time: oldTimeProto, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - return nil -} - -func (d db) ApproveTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { - return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(d.TrainingsCollection().Doc(trainingUUID)) - if err != nil { - return errors.Wrap(err, "could not find training") - } - - var training TrainingModel - err = doc.DataTo(&training) - if err != nil { - return errors.Wrap(err, "could not unmarshal training") - } - - if training.ProposedTime == nil { - return errors.New("training has no proposed time") - } - if training.MoveProposedBy == nil { - return errors.New("training has no MoveProposedBy") - } - if *training.MoveProposedBy == "trainer" && training.UserUUID != user.UUID { - return errors.Errorf("user '%s' cannot approve reschedule of user '%s'", user.UUID, training.UserUUID) - } - if *training.MoveProposedBy == user.Role { - return errors.New("reschedule cannot be accepted by requesting person") - } - - training.Time = *training.ProposedTime - training.ProposedTime = nil - - return tx.Set(d.TrainingsCollection().Doc(training.UUID), training) - }) -} - -func (d db) RejectTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { - return d.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(d.TrainingsCollection().Doc(trainingUUID)) - if err != nil { - return errors.Wrap(err, "could not find training") - } - - var training TrainingModel - err = doc.DataTo(&training) - if err != nil { - return errors.Wrap(err, "could not unmarshal training") - } - - if training.MoveProposedBy == nil { - return errors.New("training has no MoveProposedBy") - } - if *training.MoveProposedBy != "trainer" && training.UserUUID != user.UUID { - return errors.Errorf("user '%s' cannot approve reschedule of user '%s'", user.UUID, training.UserUUID) - } - - training.ProposedTime = nil - - return tx.Set(d.TrainingsCollection().Doc(training.UUID), training) - }) -} diff --git a/internal/trainings/go.mod b/internal/trainings/go.mod index ca7ae96..c2f3462 100644 --- a/internal/trainings/go.mod +++ b/internal/trainings/go.mod @@ -12,6 +12,7 @@ require ( github.com/golang/protobuf v1.3.3 github.com/google/uuid v1.1.1 github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.4.0 google.golang.org/api v0.21.0 ) diff --git a/internal/trainings/main.go b/internal/trainings/main.go index 4b1633a..f8c5d42 100644 --- a/internal/trainings/main.go +++ b/internal/trainings/main.go @@ -9,6 +9,9 @@ import ( grpcClient "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/ports" "github.com/go-chi/chi" ) @@ -33,9 +36,13 @@ func main() { } defer closeUsersClient() - firebaseDB := db{client, trainerClient, usersClient} + trainingsRepository := adapters.NewTrainingsFirestoreRepository(client) + trainerGrpc := adapters.NewTrainerGrpc(trainerClient) + usersGrpc := adapters.NewUsersGrpc(usersClient) + + trainingsService := app.NewTrainingsService(trainingsRepository, trainerGrpc, usersGrpc) server.RunHTTPServer(func(router chi.Router) http.Handler { - return HandlerFromMux(HttpServer{firebaseDB}, router) + return ports.HandlerFromMux(ports.NewHttpServer(trainingsService), router) }) } diff --git a/internal/trainings/http.go b/internal/trainings/ports/http.go similarity index 57% rename from internal/trainings/http.go rename to internal/trainings/ports/http.go index 9a9c0d9..7d7748e 100644 --- a/internal/trainings/http.go +++ b/internal/trainings/ports/http.go @@ -1,45 +1,57 @@ -package main +package ports import ( "net/http" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app" "github.com/go-chi/chi" "github.com/go-chi/render" - "github.com/google/uuid" ) type HttpServer struct { - db db + service app.TrainingService +} + +func NewHttpServer(service app.TrainingService) HttpServer { + return HttpServer{ + service: service, + } } func (h HttpServer) GetTrainings(w http.ResponseWriter, r *http.Request) { user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - trainingModels, err := h.db.GetTrainings(r.Context(), user) + var appTrainings []app.Training + if user.Role == "trainer" { + appTrainings, err = h.service.GetAllTrainings(r.Context()) + } else { + appTrainings, err = h.service.GetTrainingsForUser(r.Context(), user) + } + if err != nil { - httperr.InternalError("cannot-get-trainings", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - trainings := trainingModelsToResponse(trainingModels) + trainings := appTrainingsToResponse(appTrainings) trainingsResp := Trainings{trainings} render.Respond(w, r, trainingsResp) } -func trainingModelsToResponse(models []TrainingModel) []Training { +func appTrainingsToResponse(appTrainings []app.Training) []Training { var trainings []Training - for _, tm := range models { + for _, tm := range appTrainings { t := Training{ - CanBeCancelled: tm.canBeCancelled(), + CanBeCancelled: tm.CanBeCancelled(), MoveProposedBy: tm.MoveProposedBy, - MoveRequiresAccept: !tm.canBeCancelled(), + MoveRequiresAccept: tm.MoveRequiresAccept(), Notes: tm.Notes, ProposedTime: tm.ProposedTime, Time: tm.Time, @@ -61,33 +73,20 @@ func (h HttpServer) CreateTraining(w http.ResponseWriter, r *http.Request) { return } - // sanity check - if len(postTraining.Notes) > 1000 { - httperr.BadRequest("note-too-big", nil, w, r) - return - } - user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } + if user.Role != "attendee" { httperr.Unauthorised("invalid-role", nil, w, r) return } - training := TrainingModel{ - Notes: postTraining.Notes, - Time: postTraining.Time, - User: user.DisplayName, - UserUUID: user.UUID, - UUID: uuid.New().String(), - } - - err = h.db.CreateTraining(r.Context(), user, training) + err = h.service.CreateTraining(r.Context(), user, postTraining.Time, postTraining.Notes) if err != nil { - httperr.InternalError("cannot-create-training", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } } @@ -97,13 +96,13 @@ func (h HttpServer) CancelTraining(w http.ResponseWriter, r *http.Request) { user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - err = h.db.CancelTraining(r.Context(), user, trainingUUID) + err = h.service.CancelTraining(r.Context(), user, trainingUUID) if err != nil { - httperr.InternalError("cannot-update-training", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } } @@ -117,21 +116,15 @@ func (h HttpServer) RescheduleTraining(w http.ResponseWriter, r *http.Request) { return } - // sanity check - if len(rescheduleTraining.Notes) > 1000 { - httperr.BadRequest("note-too-big", nil, w, r) - return - } - user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - err = h.db.RescheduleTraining(r.Context(), user, trainingUUID, rescheduleTraining.Time, rescheduleTraining.Notes) + err = h.service.RescheduleTraining(r.Context(), user, trainingUUID, rescheduleTraining.Time, rescheduleTraining.Notes) if err != nil { - httperr.InternalError("cannot-update-training", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } } @@ -141,13 +134,13 @@ func (h HttpServer) ApproveRescheduleTraining(w http.ResponseWriter, r *http.Req user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - err = h.db.ApproveTrainingReschedule(r.Context(), user, trainingUUID) + err = h.service.ApproveTrainingReschedule(r.Context(), user, trainingUUID) if err != nil { - httperr.InternalError("cannot-update-training", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } } @@ -157,13 +150,13 @@ func (h HttpServer) RejectRescheduleTraining(w http.ResponseWriter, r *http.Requ user, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } - err = h.db.RejectTrainingReschedule(r.Context(), user, trainingUUID) + err = h.service.RejectTrainingReschedule(r.Context(), user, trainingUUID) if err != nil { - httperr.InternalError("cannot-update-training", err, w, r) + httperr.RespondWithSlugError(err, w, r) return } } diff --git a/internal/trainings/openapi_api.gen.go b/internal/trainings/ports/openapi_api.gen.go similarity index 98% rename from internal/trainings/openapi_api.gen.go rename to internal/trainings/ports/openapi_api.gen.go index e3d2f99..c0c01a3 100644 --- a/internal/trainings/openapi_api.gen.go +++ b/internal/trainings/ports/openapi_api.gen.go @@ -1,14 +1,15 @@ -// Package main provides primitives to interact the openapi HTTP API. +// Package ports provides primitives to interact the openapi HTTP API. // // Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. -package main +package ports import ( "context" "fmt" + "net/http" + "github.com/deepmap/oapi-codegen/pkg/runtime" "github.com/go-chi/chi" - "net/http" ) type ServerInterface interface { diff --git a/internal/trainings/openapi_types.gen.go b/internal/trainings/ports/openapi_types.gen.go similarity index 94% rename from internal/trainings/openapi_types.gen.go rename to internal/trainings/ports/openapi_types.gen.go index 7ea0834..ba23ac3 100644 --- a/internal/trainings/openapi_types.gen.go +++ b/internal/trainings/ports/openapi_types.gen.go @@ -1,7 +1,7 @@ -// Package main provides primitives to interact the openapi HTTP API. +// Package ports provides primitives to interact the openapi HTTP API. // // Code generated by github.com/deepmap/oapi-codegen DO NOT EDIT. -package main +package ports import ( "time" diff --git a/internal/users/firestore.go b/internal/users/firestore.go index c2e0b0b..336967c 100644 --- a/internal/users/firestore.go +++ b/internal/users/firestore.go @@ -20,12 +20,12 @@ type db struct { firestoreClient *firestore.Client } -func (d db) UsersCollection() *firestore.CollectionRef { +func (d db) usersCollection() *firestore.CollectionRef { return d.firestoreClient.Collection("users") } func (d db) UserDocumentRef(userID string) *firestore.DocumentRef { - return d.UsersCollection().Doc(userID) + return d.usersCollection().Doc(userID) } func (d db) GetUser(ctx context.Context, userID string) (UserModel, error) { diff --git a/internal/users/http.go b/internal/users/http.go index 372b5ca..4375170 100644 --- a/internal/users/http.go +++ b/internal/users/http.go @@ -16,7 +16,7 @@ type HttpServer struct { func (h HttpServer) GetCurrentUser(w http.ResponseWriter, r *http.Request) { authUser, err := auth.UserFromCtx(r.Context()) if err != nil { - httperr.Unauthorised("no-user-found", err, w, r) + httperr.RespondWithSlugError(err, w, r) return }