diff --git a/internal/trainings/firestore.go b/internal/trainings/firestore.go index a115afd..5d836cb 100644 --- a/internal/trainings/firestore.go +++ b/internal/trainings/firestore.go @@ -7,6 +7,10 @@ import ( "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" ) @@ -28,6 +32,8 @@ func (t TrainingModel) canBeCancelled() bool { type db struct { firestoreClient *firestore.Client + trainerClient trainer.TrainerServiceClient + usersClient users.UsersServiceClient } func (d db) TrainingsCollection() *firestore.CollectionRef { @@ -66,3 +72,225 @@ func (d db) GetTrainings(ctx context.Context, user auth.User) ([]TrainingModel, 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/http.go b/internal/trainings/http.go index 40eeed8..9a9c0d9 100644 --- a/internal/trainings/http.go +++ b/internal/trainings/http.go @@ -1,26 +1,17 @@ package main import ( - "context" "net/http" - "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/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" "github.com/go-chi/chi" "github.com/go-chi/render" - "github.com/golang/protobuf/ptypes" "github.com/google/uuid" - "github.com/pkg/errors" ) type HttpServer struct { - db db - trainerClient trainer.TrainerServiceClient - usersClient users.UsersServiceClient + db db } func (h HttpServer) GetTrainings(w http.ResponseWriter, r *http.Request) { @@ -86,7 +77,7 @@ func (h HttpServer) CreateTraining(w http.ResponseWriter, r *http.Request) { return } - training := &TrainingModel{ + training := TrainingModel{ Notes: postTraining.Notes, Time: postTraining.Time, User: user.DisplayName, @@ -94,38 +85,7 @@ func (h HttpServer) CreateTraining(w http.ResponseWriter, r *http.Request) { UUID: uuid.New().String(), } - collection := h.db.TrainingsCollection() - - err = h.db.firestoreClient.RunTransaction(r.Context(), 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 = h.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 = h.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) - }) + err = h.db.CreateTraining(r.Context(), user, training) if err != nil { httperr.InternalError("cannot-create-training", err, w, r) return @@ -141,63 +101,7 @@ func (h HttpServer) CancelTraining(w http.ResponseWriter, r *http.Request) { return } - trainingsCollection := h.db.TrainingsCollection() - - err = h.db.firestoreClient.RunTransaction(r.Context(), 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 := h.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 = h.trainerClient.CancelTraining(ctx, &trainer.UpdateHourRequest{ - Time: timestamp, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - return tx.Delete(trainingDocumentRef) - }) + err = h.db.CancelTraining(r.Context(), user, trainingUUID) if err != nil { httperr.InternalError("cannot-update-training", err, w, r) return @@ -225,44 +129,7 @@ func (h HttpServer) RescheduleTraining(w http.ResponseWriter, r *http.Request) { return } - collection := h.db.TrainingsCollection() - - err = h.db.firestoreClient.RunTransaction(r.Context(), func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(h.db.TrainingsCollection().Doc(trainingUUID)) - if err != nil { - return errors.Wrap(err, "could not find training") - } - - docs, err := tx.Documents(collection.Where("Time", "==", rescheduleTraining.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", rescheduleTraining.Time) - } - - var training TrainingModel - err = doc.DataTo(&training) - if err != nil { - return errors.Wrap(err, "could not unmarshal training") - } - - if training.canBeCancelled() { - err = h.rescheduleTraining(ctx, training.Time, rescheduleTraining.Time) - if err != nil { - return errors.Wrap(err, "unable to reschedule training") - } - - training.Time = rescheduleTraining.Time - training.Notes = rescheduleTraining.Notes - } else { - training.ProposedTime = &rescheduleTraining.Time - training.MoveProposedBy = &user.Role - training.Notes = rescheduleTraining.Notes - } - - return tx.Set(collection.Doc(training.UUID), training) - }) + err = h.db.RescheduleTraining(r.Context(), user, trainingUUID, rescheduleTraining.Time, rescheduleTraining.Notes) if err != nil { httperr.InternalError("cannot-update-training", err, w, r) return @@ -278,36 +145,7 @@ func (h HttpServer) ApproveRescheduleTraining(w http.ResponseWriter, r *http.Req return } - err = h.db.firestoreClient.RunTransaction(r.Context(), func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(h.db.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(h.db.TrainingsCollection().Doc(training.UUID), training) - }) + err = h.db.ApproveTrainingReschedule(r.Context(), user, trainingUUID) if err != nil { httperr.InternalError("cannot-update-training", err, w, r) return @@ -323,59 +161,9 @@ func (h HttpServer) RejectRescheduleTraining(w http.ResponseWriter, r *http.Requ return } - err = h.db.firestoreClient.RunTransaction(r.Context(), func(ctx context.Context, tx *firestore.Transaction) error { - doc, err := tx.Get(h.db.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(h.db.TrainingsCollection().Doc(training.UUID), training) - }) + err = h.db.RejectTrainingReschedule(r.Context(), user, trainingUUID) if err != nil { httperr.InternalError("cannot-update-training", err, w, r) return } } - -func (h HttpServer) 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 = h.trainerClient.ScheduleTraining(ctx, &trainer.UpdateHourRequest{ - Time: newTimeProto, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - _, err = h.trainerClient.CancelTraining(ctx, &trainer.UpdateHourRequest{ - Time: oldTimeProto, - }) - if err != nil { - return errors.Wrap(err, "unable to update trainer hour") - } - - return nil -} diff --git a/internal/trainings/main.go b/internal/trainings/main.go index 0667d98..4b1633a 100644 --- a/internal/trainings/main.go +++ b/internal/trainings/main.go @@ -33,9 +33,9 @@ func main() { } defer closeUsersClient() - firebaseDB := db{client} + firebaseDB := db{client, trainerClient, usersClient} server.RunHTTPServer(func(router chi.Router) http.Handler { - return HandlerFromMux(HttpServer{firebaseDB, trainerClient, usersClient}, router) + return HandlerFromMux(HttpServer{firebaseDB}, router) }) }