From 16ff6dcbcfce86158752ed60c66d1423c5a21c9a Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sat, 4 Oct 2025 10:56:03 +0700 Subject: [PATCH 01/32] added NewInterview service function and converted funcs to methods --- handlers/handlers.go | 20 ++++++++------------ handlers/model.go | 6 +++--- internal/server/server.go | 3 ++- interview/model.go | 20 ++++++++++++++++++++ interview/service.go | 24 ++++++++++-------------- 5 files changed, 43 insertions(+), 30 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 8c34f36..ba25ab2 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -536,11 +536,7 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { return } - interviewStarted, err := interview.StartInterview( - h.InterviewRepo, - h.UserRepo, - h.BillingRepo, - h.OpenAI, + interviewStarted, err := h.InterviewService.StartInterview( userReturned, 30, 3, @@ -569,7 +565,7 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { return } - err = interview.LinkConversation(h.InterviewRepo, interviewStarted.Id, conversationID) + err = h.InterviewService.LinkConversation(interviewStarted.Id, conversationID) if err != nil { h.Logger.Error("interview.LinkConversation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") @@ -604,7 +600,7 @@ func (h *Handler) GetInterviewHandler(w http.ResponseWriter, r *http.Request) { return } - interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) + interviewReturned, err := h.InterviewService.GetInterview(interviewID) if err != nil { h.Logger.Error("GetInterview failed", "error", err) RespondWithError(w, http.StatusNotFound, "Interview not found") @@ -649,7 +645,7 @@ func (h *Handler) UpdateInterviewStatusHandler(w http.ResponseWriter, r *http.Re return } - interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) + interviewReturned, err := h.InterviewService.GetInterview(interviewID) if err != nil { h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") @@ -663,7 +659,7 @@ func (h *Handler) UpdateInterviewStatusHandler(w http.ResponseWriter, r *http.Re return } - err = h.InterviewRepo.UpdateStatus(interviewID, userID, payload.Status) + err = h.InterviewService.InterviewRepo.UpdateStatus(interviewID, userID, payload.Status) if err != nil { h.Logger.Error("UpdateInterviewStatus failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not update status") @@ -701,7 +697,7 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ return } - interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) + interviewReturned, err := h.InterviewService.GetInterview(interviewID) if err != nil { h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") @@ -785,7 +781,7 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ return } - interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) + interviewReturned, err := h.InterviewService.GetInterview(interviewID) if err != nil { h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") @@ -854,7 +850,7 @@ func (h *Handler) GetConversationHandler(w http.ResponseWriter, r *http.Request) return } - interviewReturned, err := interview.GetInterview(h.InterviewRepo, interviewID) + interviewReturned, err := h.InterviewService.GetInterview(interviewID) if err != nil { h.Logger.Error("GetInterview error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") diff --git a/handlers/model.go b/handlers/model.go index be887d3..19ced55 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -54,7 +54,7 @@ type ReturnVals struct { type Handler struct { UserRepo user.UserRepo - InterviewRepo interview.InterviewRepo + InterviewService *interview.InterviewService ConversationRepo conversation.ConversationRepo TokenRepo token.TokenRepo BillingRepo billing.BillingRepo @@ -66,7 +66,7 @@ type Handler struct { } func NewHandler( - interviewRepo interview.InterviewRepo, + interviewService *interview.InterviewService, userRepo user.UserRepo, tokenRepo token.TokenRepo, conversationRepo conversation.ConversationRepo, @@ -77,7 +77,7 @@ func NewHandler( db *sql.DB, logger *slog.Logger) *Handler { return &Handler{ - InterviewRepo: interviewRepo, + InterviewService: interviewService, UserRepo: userRepo, TokenRepo: tokenRepo, ConversationRepo: conversationRepo, diff --git a/internal/server/server.go b/internal/server/server.go index ec7f53c..e74404b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -36,6 +36,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) openAI := chatgpt.NewOpenAI(logger) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI) mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(logger) if err != nil { @@ -43,7 +44,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { return nil, err } - handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/interview/model.go b/interview/model.go index aa76139..b586822 100644 --- a/interview/model.go +++ b/interview/model.go @@ -3,6 +3,10 @@ package interview import ( "errors" "time" + + "github.com/michaelboegner/interviewer/billing" + "github.com/michaelboegner/interviewer/chatgpt" + "github.com/michaelboegner/interviewer/user" ) type Interview struct { @@ -31,8 +35,24 @@ type Summary struct { Score *int `json:"score,omitempty"` } +type InterviewService struct { + InterviewRepo InterviewRepo `json:"interview_repo,omitempty"` + UserRepo user.UserRepo `json:"user_repo,omitempty"` + BillingRepo billing.BillingRepo `json:"billing_repo,omitempty"` + AI chatgpt.AIClient `jaon:"ai,omitempty"` +} + var ErrNoValidCredits = errors.New("no valid credits") +func NewInterview(interviewRepo InterviewRepo, userRepo user.UserRepo, billingRepo billing.BillingRepo, ai chatgpt.AIClient) *InterviewService { + return &InterviewService{ + InterviewRepo: interviewRepo, + UserRepo: userRepo, + BillingRepo: billingRepo, + AI: ai, + } +} + type InterviewRepo interface { LinkConversation(interviewID, conversationID int) error CreateInterview(interview *Interview) (int, error) diff --git a/interview/service.go b/interview/service.go index 6278c4b..d00e29d 100644 --- a/interview/service.go +++ b/interview/service.go @@ -10,18 +10,14 @@ import ( "github.com/michaelboegner/interviewer/user" ) -func StartInterview( - interviewRepo InterviewRepo, - userRepo user.UserRepo, - billingRepo billing.BillingRepo, - ai chatgpt.AIClient, +func (i *InterviewService) StartInterview( user *user.User, length, numberQuestions int, difficulty string, jd string) (*Interview, error) { - err := deductAndLogCredit(user, userRepo, billingRepo) + err := deductAndLogCredit(user, i.UserRepo, i.BillingRepo) if err != nil { log.Printf("checkCreditsLogTransaction failed: %v", err) return nil, err @@ -31,12 +27,12 @@ func StartInterview( jdSummary := "" if jd != "" { - jdInput, err := ai.ExtractJDInput(jd) + jdInput, err := i.AI.ExtractJDInput(jd) if err != nil { fmt.Printf("ai.ExtractJDInput() failed: %v", err) return nil, err } - jdSummary, err = ai.ExtractJDSummary(jdInput) + jdSummary, err = i.AI.ExtractJDSummary(jdInput) if err != nil { fmt.Printf("ai.ExtractJDSummary() failed: %v", err) return nil, err @@ -45,7 +41,7 @@ func StartInterview( prompt := chatgpt.BuildPrompt([]string{}, "Introduction", 1, jdSummary) - chatGPTResponse, err := ai.GetChatGPTResponse(prompt) + chatGPTResponse, err := i.AI.GetChatGPTResponse(prompt) if err != nil { log.Printf("getChatGPTResponse err: %v\n", err) return nil, err @@ -67,7 +63,7 @@ func StartInterview( UpdatedAt: now, } - id, err := interviewRepo.CreateInterview(interview) + id, err := i.InterviewRepo.CreateInterview(interview) if err != nil { log.Printf("CreateInterview err: %v", err) return nil, err @@ -77,8 +73,8 @@ func StartInterview( return interview, nil } -func LinkConversation(interviewRepo InterviewRepo, interviewID, conversationID int) error { - err := interviewRepo.LinkConversation(interviewID, conversationID) +func (i *InterviewService) LinkConversation(interviewID, conversationID int) error { + err := i.InterviewRepo.LinkConversation(interviewID, conversationID) if err != nil { log.Printf("interviewRepo.LinkConversation failed: %v", err) return err @@ -87,8 +83,8 @@ func LinkConversation(interviewRepo InterviewRepo, interviewID, conversationID i return nil } -func GetInterview(interviewRepo InterviewRepo, interviewID int) (*Interview, error) { - interview, err := interviewRepo.GetInterview(interviewID) +func (i *InterviewService) GetInterview(interviewID int) (*Interview, error) { + interview, err := i.InterviewRepo.GetInterview(interviewID) if err != nil { return nil, err } From b12e42e5045eb385b9a4bd2d30cd870c8e0d9b21 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sat, 4 Oct 2025 11:18:41 +0700 Subject: [PATCH 02/32] fixed integration and unit tests --- handlers/handlers_test.go | 2 +- internal/testutil/server.go | 3 ++- interview/service_test.go | 12 ++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 7e1d311..4638923 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -749,7 +749,7 @@ func Test_InterviewsHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - interviewReturned, err := interview.GetInterview(Handler.InterviewRepo, respUnmarshalled.InterviewID) + interviewReturned, err := Handler.InterviewService.GetInterview(respUnmarshalled.InterviewID) if err != nil { t.Fatalf("Assert Database: GetInterview failed: %v", err) } diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 081bf42..1e93dd1 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -40,12 +40,13 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { openAI := mocks.NewMockOpenAIClient() mailer := mocks.NewMockMailer() billing, err := billing.NewBilling(logger) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) diff --git a/interview/service_test.go b/interview/service_test.go index d84e7a1..4c77989 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -83,13 +83,10 @@ func TestStartInterview(t *testing.T) { repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() + interviewService := interview.NewInterview(repo, userRepo, billingRepo, tc.aiClient) repo.FailRepo = tc.failRepo - interviewStarted, err := interview.StartInterview( - repo, - userRepo, - billingRepo, - tc.aiClient, + interviewStarted, err := interviewService.StartInterview( tc.user, tc.length, tc.numQuestions, @@ -173,6 +170,9 @@ func TestGetInterview(t *testing.T) { defer showLogsIfFail(t, tc.name, buf) repo := interview.NewMockRepo() + userRepo := user.NewMockRepo() + billingRepo := billing.NewMockRepo() + interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIClient{}) repo.FailRepo = tc.failRepo if tc.setup != nil { @@ -182,7 +182,7 @@ func TestGetInterview(t *testing.T) { } } - got, err := interview.GetInterview(repo, tc.interviewID) + got, err := interviewService.GetInterview(tc.interviewID) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") From 7761284474989ef29ab9f0aed519399605194a67 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sun, 5 Oct 2025 12:24:41 +0700 Subject: [PATCH 03/32] added logger to new Interview service and service_test --- internal/server/server.go | 2 +- internal/testutil/server.go | 2 +- interview/model.go | 5 ++++- interview/service.go | 37 +++++++++++++++++++++---------------- interview/service_test.go | 13 +++++-------- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index e74404b..b5bc204 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -36,7 +36,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) openAI := chatgpt.NewOpenAI(logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(logger) if err != nil { diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 1e93dd1..84b61b0 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -40,7 +40,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { openAI := mocks.NewMockOpenAIClient() mailer := mocks.NewMockMailer() billing, err := billing.NewBilling(logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err diff --git a/interview/model.go b/interview/model.go index b586822..5ae9847 100644 --- a/interview/model.go +++ b/interview/model.go @@ -2,6 +2,7 @@ package interview import ( "errors" + "log/slog" "time" "github.com/michaelboegner/interviewer/billing" @@ -40,16 +41,18 @@ type InterviewService struct { UserRepo user.UserRepo `json:"user_repo,omitempty"` BillingRepo billing.BillingRepo `json:"billing_repo,omitempty"` AI chatgpt.AIClient `jaon:"ai,omitempty"` + Logger *slog.Logger } var ErrNoValidCredits = errors.New("no valid credits") -func NewInterview(interviewRepo InterviewRepo, userRepo user.UserRepo, billingRepo billing.BillingRepo, ai chatgpt.AIClient) *InterviewService { +func NewInterview(interviewRepo InterviewRepo, userRepo user.UserRepo, billingRepo billing.BillingRepo, ai chatgpt.AIClient, logger *slog.Logger) *InterviewService { return &InterviewService{ InterviewRepo: interviewRepo, UserRepo: userRepo, BillingRepo: billingRepo, AI: ai, + Logger: logger, } } diff --git a/interview/service.go b/interview/service.go index d00e29d..b1a4e33 100644 --- a/interview/service.go +++ b/interview/service.go @@ -2,7 +2,7 @@ package interview import ( "fmt" - "log" + "log/slog" "time" "github.com/michaelboegner/interviewer/billing" @@ -17,9 +17,9 @@ func (i *InterviewService) StartInterview( difficulty string, jd string) (*Interview, error) { - err := deductAndLogCredit(user, i.UserRepo, i.BillingRepo) + err := deductAndLogCredit(user, i.UserRepo, i.BillingRepo, i.Logger) if err != nil { - log.Printf("checkCreditsLogTransaction failed: %v", err) + i.Logger.Error("checkCreditsLogTransaction failed", "error", err) return nil, err } @@ -29,12 +29,12 @@ func (i *InterviewService) StartInterview( if jd != "" { jdInput, err := i.AI.ExtractJDInput(jd) if err != nil { - fmt.Printf("ai.ExtractJDInput() failed: %v", err) + i.Logger.Error("ai.ExtractJDInput() failed", "error", err) return nil, err } jdSummary, err = i.AI.ExtractJDSummary(jdInput) if err != nil { - fmt.Printf("ai.ExtractJDSummary() failed: %v", err) + i.Logger.Error("ai.ExtractJDSummary() failed", "error", err) return nil, err } } @@ -43,7 +43,7 @@ func (i *InterviewService) StartInterview( chatGPTResponse, err := i.AI.GetChatGPTResponse(prompt) if err != nil { - log.Printf("getChatGPTResponse err: %v\n", err) + i.Logger.Error("getChatGPTResponse err", "error", err) return nil, err } @@ -65,7 +65,7 @@ func (i *InterviewService) StartInterview( id, err := i.InterviewRepo.CreateInterview(interview) if err != nil { - log.Printf("CreateInterview err: %v", err) + i.Logger.Error("CreateInterview err", "error", err) return nil, err } interview.Id = id @@ -76,7 +76,7 @@ func (i *InterviewService) StartInterview( func (i *InterviewService) LinkConversation(interviewID, conversationID int) error { err := i.InterviewRepo.LinkConversation(interviewID, conversationID) if err != nil { - log.Printf("interviewRepo.LinkConversation failed: %v", err) + i.Logger.Error("interviewRepo.LinkConversation failed", "error", err) return err } @@ -86,13 +86,14 @@ func (i *InterviewService) LinkConversation(interviewID, conversationID int) err func (i *InterviewService) GetInterview(interviewID int) (*Interview, error) { interview, err := i.InterviewRepo.GetInterview(interviewID) if err != nil { + i.Logger.Error("interviewRepo.GetInterview failed", "error", err) return nil, err } return interview, nil } -func canUseCredit(user *user.User) (string, error) { +func canUseCredit(user *user.User, logger *slog.Logger) (string, error) { now := time.Now() switch { @@ -100,27 +101,31 @@ func canUseCredit(user *user.User) (string, error) { user.SubscriptionEndDate.After(now) && user.SubscriptionStatus != "expired" && user.SubscriptionCredits > 0: + logger.Info("subscrtipion plan in canUseCredit check") return "subscription", nil case user.IndividualCredits > 0: + logger.Info("individual plan in canUseCredit check") return "individual", nil default: + logger.Info("no valid credits in canUseCredit check") return "", ErrNoValidCredits } } -func deductAndLogCredit(user *user.User, userRepo user.UserRepo, billingRepo billing.BillingRepo) error { - creditType, err := canUseCredit(user) +func deductAndLogCredit(user *user.User, userRepo user.UserRepo, billingRepo billing.BillingRepo, logger *slog.Logger) error { + creditType, err := canUseCredit(user, logger) if err != nil { - log.Print("canUseCredit failed", err) + logger.Error("canUseCredit failed", "error", err) return err } - if creditType != "" { - + if creditType == "" { + logger.Info("user doesn't have a valid plan or credits") + return fmt.Errorf("user doesn't have a valid plan or credits") } err = userRepo.AddCredits(user.ID, -1, creditType) if err != nil { - log.Printf("AddCredits failed: %v", err) + logger.Error("AddCredits failed", "error", err) return err } @@ -132,7 +137,7 @@ func deductAndLogCredit(user *user.User, userRepo user.UserRepo, billingRepo bil Reason: reason, } if err := billingRepo.LogCreditTransaction(tx); err != nil { - log.Printf("billingRepo.LogCreditTransaction failed: %v", err) + logger.Error("billingRepo.LogCreditTransaction failed", "error", err) return err } diff --git a/interview/service_test.go b/interview/service_test.go index 4c77989..a7a2e90 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -3,6 +3,7 @@ package interview_test import ( "fmt" "log" + "log/slog" "os" "strings" "testing" @@ -77,13 +78,11 @@ func TestStartInterview(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, tc.aiClient) + interviewService := interview.NewInterview(repo, userRepo, billingRepo, tc.aiClient, logger) repo.FailRepo = tc.failRepo interviewStarted, err := interviewService.StartInterview( @@ -166,13 +165,11 @@ func TestGetInterview(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIClient{}) + interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIClient{}, logger) repo.FailRepo = tc.failRepo if tc.setup != nil { From 4c40218664a36d64e9db238431f4113c45f9bd32 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sun, 5 Oct 2025 12:25:33 +0700 Subject: [PATCH 04/32] removed unnecessary function --- interview/service_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/interview/service_test.go b/interview/service_test.go index a7a2e90..c25d537 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -1,10 +1,7 @@ package interview_test import ( - "fmt" - "log" "log/slog" - "os" "strings" "testing" "time" @@ -200,10 +197,3 @@ func TestGetInterview(t *testing.T) { }) } } - -func showLogsIfFail(t *testing.T, name string, buf strings.Builder) { - log.SetOutput(os.Stderr) - if t.Failed() { - fmt.Printf("---- logs for test: %s ----\n%s\n", name, buf.String()) - } -} From 73d3baf95512f085c0fef9ba0a6b5daf9cc418fb Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 6 Oct 2025 14:33:58 +0700 Subject: [PATCH 05/32] converted user package services to methods --- handlers/model.go | 6 +-- internal/server/server.go | 3 +- interview/model.go | 8 ++-- user/model.go | 13 ++++++ user/service.go | 86 +++++++++++++++++++-------------------- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/handlers/model.go b/handlers/model.go index 19ced55..079cc32 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -53,7 +53,7 @@ type ReturnVals struct { } type Handler struct { - UserRepo user.UserRepo + UserService user.UserService InterviewService *interview.InterviewService ConversationRepo conversation.ConversationRepo TokenRepo token.TokenRepo @@ -67,7 +67,7 @@ type Handler struct { func NewHandler( interviewService *interview.InterviewService, - userRepo user.UserRepo, + userService user.UserService, tokenRepo token.TokenRepo, conversationRepo conversation.ConversationRepo, billingRepo billing.BillingRepo, @@ -78,7 +78,7 @@ func NewHandler( logger *slog.Logger) *Handler { return &Handler{ InterviewService: interviewService, - UserRepo: userRepo, + UserService: userService, TokenRepo: tokenRepo, ConversationRepo: conversationRepo, BillingRepo: billingRepo, diff --git a/internal/server/server.go b/internal/server/server.go index b5bc204..d02ab41 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -37,6 +37,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { billingRepo := billing.NewRepository(db) openAI := chatgpt.NewOpenAI(logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) + userService := user mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(logger) if err != nil { @@ -44,7 +45,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { return nil, err } - handler := handlers.NewHandler(interviewService, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/interview/model.go b/interview/model.go index 5ae9847..c5331ca 100644 --- a/interview/model.go +++ b/interview/model.go @@ -37,10 +37,10 @@ type Summary struct { } type InterviewService struct { - InterviewRepo InterviewRepo `json:"interview_repo,omitempty"` - UserRepo user.UserRepo `json:"user_repo,omitempty"` - BillingRepo billing.BillingRepo `json:"billing_repo,omitempty"` - AI chatgpt.AIClient `jaon:"ai,omitempty"` + InterviewRepo InterviewRepo + UserRepo user.UserRepo + BillingRepo billing.BillingRepo + AI chatgpt.AIClient Logger *slog.Logger } diff --git a/user/model.go b/user/model.go index 1945dd7..ee866c4 100644 --- a/user/model.go +++ b/user/model.go @@ -2,6 +2,7 @@ package user import ( "errors" + "log/slog" "time" "github.com/golang-jwt/jwt/v5" @@ -36,6 +37,18 @@ type EmailClaims struct { jwt.RegisteredClaims } +type UserService struct { + UserRepo UserRepo + Logger *slog.Logger +} + +func NewUserService(userRepo UserRepo, logger *slog.Logger) *UserService { + return &UserService{ + UserRepo: userRepo, + Logger: logger, + } +} + type UserRepo interface { CreateUser(user *User) (int, error) MarkUserDeleted(userID int) error diff --git a/user/service.go b/user/service.go index fe8be97..d273afa 100644 --- a/user/service.go +++ b/user/service.go @@ -12,7 +12,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -func VerificationToken(email, username, password string) (string, error) { +func (u *UserService) VerificationToken(email, username, password string) (string, error) { passwordHashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost) if err != nil { return "", err @@ -30,7 +30,7 @@ func VerificationToken(email, username, password string) (string, error) { SignedString([]byte(os.Getenv("JWT_SECRET"))) } -func CreateUser(repo UserRepo, tokenStr string) (*User, error) { +func (u *UserService) CreateUser(tokenStr string) (*User, error) { claims := &EmailClaims{} tkn, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("JWT_SECRET")), nil @@ -52,7 +52,7 @@ func CreateUser(repo UserRepo, tokenStr string) (*User, error) { UpdatedAt: time.Now().UTC(), } - id, err := repo.CreateUser(user) + id, err := u.UserRepo.CreateUser(user) if err != nil { return nil, err } @@ -60,15 +60,15 @@ func CreateUser(repo UserRepo, tokenStr string) (*User, error) { return user, nil } -func LoginUser(repo UserRepo, email, password string) (string, string, int, error) { - userID, hashedPassword, err := repo.GetPasswordandID(email) +func (u *UserService) LoginUser(email, password string) (string, string, int, error) { + userID, hashedPassword, err := u.UserRepo.GetPasswordandID(email) if err != nil { return "", "", 0, err } - user, err := repo.GetUser(userID) + user, err := u.UserRepo.GetUser(userID) if err != nil { - log.Printf("repo.GetUser failed: %v", err) + log.Printf("u.UserRepo.GetUser failed: %v", err) return "", "", 0, err } @@ -90,8 +90,8 @@ func LoginUser(repo UserRepo, email, password string) (string, string, int, erro return jwToken, user.Username, userID, nil } -func GetUser(repo UserRepo, userID int) (*User, error) { - userReturned, err := repo.GetUser(userID) +func (u *UserService) GetUser(userID int) (*User, error) { + userReturned, err := u.UserRepo.GetUser(userID) if err != nil { log.Printf("GetUser failed: %v", err) return nil, err @@ -99,28 +99,28 @@ func GetUser(repo UserRepo, userID int) (*User, error) { return userReturned, nil } -func MarkUserDeleted(repo UserRepo, userId int) error { - err := repo.MarkUserDeleted(userId) +func (u *UserService) MarkUserDeleted(userId int) error { + err := u.UserRepo.MarkUserDeleted(userId) if err != nil { - log.Printf("repo.DeleteUser failed: %v", err) + log.Printf("u.UserRepo.DeleteUser failed: %v", err) return err } return nil } -func GetUserByEmail(repo UserRepo, email string) error { - _, err := repo.GetUserByEmail(email) +func (u *UserService) GetUserByEmail(email string) error { + _, err := u.UserRepo.GetUserByEmail(email) if err != nil { - log.Printf("repo.GetUserByEmail failed: %v", err) + log.Printf("u.UserRepo.GetUserByEmail failed: %v", err) return err } return nil } -func RequestPasswordReset(repo UserRepo, email string) (string, error) { - user, err := repo.GetUserByEmail(email) +func (u *UserService) RequestPasswordReset(email string) (string, error) { + user, err := u.UserRepo.GetUserByEmail(email) if err != nil { log.Printf("GetUserByEmail failed: %v", err) return "", err @@ -135,7 +135,7 @@ func RequestPasswordReset(repo UserRepo, email string) (string, error) { return resetJWT, nil } -func ResetPassword(repo UserRepo, newPassword string, resetJWT string) error { +func (u *UserService) ResetPassword(newPassword string, resetJWT string) error { email, err := verifyResetToken(resetJWT) if err != nil { return err @@ -147,7 +147,7 @@ func ResetPassword(repo UserRepo, newPassword string, resetJWT string) error { return err } - err = repo.UpdatePasswordByEmail(email, passwordHashed) + err = u.UserRepo.UpdatePasswordByEmail(email, passwordHashed) if err != nil { log.Printf("UpdatePasswordByEmail failed: %v", err) return err @@ -156,29 +156,8 @@ func ResetPassword(repo UserRepo, newPassword string, resetJWT string) error { return nil } -func verifyResetToken(tokenString string) (string, error) { - jwtSecret := os.Getenv("JWT_SECRET") - if jwtSecret == "" { - log.Printf("JWT secret is not set") - err := errors.New("jwt secret is not set") - return "", err - } - - token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(jwtSecret), nil - }) - if err != nil { - return "", err - } - if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid { - return claims.Subject, nil - } else { - return "", errors.New("Invalid token") - } -} - -func GetOrCreateByEmail(repo UserRepo, email, username string) (*User, error) { - user, err := repo.GetUserByEmail(email) +func (u *UserService) GetOrCreateByEmail(email, username string) (*User, error) { + user, err := u.UserRepo.GetUserByEmail(email) if err == nil { return user, nil } @@ -193,7 +172,7 @@ func GetOrCreateByEmail(repo UserRepo, email, username string) (*User, error) { UpdatedAt: time.Now().UTC(), } - id, err := repo.CreateUser(newUser) + id, err := u.UserRepo.CreateUser(newUser) if err != nil { log.Printf("CreateUser failed: %v", err) return nil, err @@ -202,3 +181,24 @@ func GetOrCreateByEmail(repo UserRepo, email, username string) (*User, error) { newUser.ID = id return newUser, nil } + +func verifyResetToken(tokenString string) (string, error) { + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + log.Printf("JWT secret is not set") + err := errors.New("jwt secret is not set") + return "", err + } + + token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + if err != nil { + return "", err + } + if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid { + return claims.Subject, nil + } else { + return "", errors.New("Invalid token") + } +} From 20673ef9f48cb0d7d655664903bf644e09a8e18d Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 7 Oct 2025 10:58:45 +0700 Subject: [PATCH 06/32] added user service instantiation to server.go --- handlers/model.go | 4 ++-- internal/server/server.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/model.go b/handlers/model.go index 079cc32..1d30827 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -53,7 +53,7 @@ type ReturnVals struct { } type Handler struct { - UserService user.UserService + UserService *user.UserService InterviewService *interview.InterviewService ConversationRepo conversation.ConversationRepo TokenRepo token.TokenRepo @@ -67,7 +67,7 @@ type Handler struct { func NewHandler( interviewService *interview.InterviewService, - userService user.UserService, + userService *user.UserService, tokenRepo token.TokenRepo, conversationRepo conversation.ConversationRepo, billingRepo billing.BillingRepo, diff --git a/internal/server/server.go b/internal/server/server.go index d02ab41..8658cf0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -37,7 +37,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { billingRepo := billing.NewRepository(db) openAI := chatgpt.NewOpenAI(logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) - userService := user + userService := user.NewUserService(userRepo, logger) mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(logger) if err != nil { From 7ab98fa79001e82babb6a28e5b0869f703a39f2c Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 7 Oct 2025 11:05:37 +0700 Subject: [PATCH 07/32] fixed handlers_test to use new user service --- handlers/handlers_test.go | 20 ++++++++++---------- internal/testutil/helpers.go | 4 ++-- internal/testutil/server.go | 3 ++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 4638923..90776e8 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -178,7 +178,7 @@ func Test_RequestVerificationHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - user, err := user.GetUser(Handler.UserRepo, got.UserID) + user, err := Handler.UserService.GetUser(got.UserID) if err != nil { t.Fatalf("Assert Database: GetUser failed: %v", err) } @@ -227,7 +227,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - verificationJWT, err := user.VerificationToken(tc.email, tc.username, tc.password) + verificationJWT, err := Handler.UserService.VerificationToken(tc.email, tc.username, tc.password) if err != nil { t.Fatalf("GenerateEmailVerificationToken failed: %v", err) } @@ -269,7 +269,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - user, err := user.GetUser(Handler.UserRepo, got.UserID) + user, err := Handler.UserService.GetUser(got.UserID) if err != nil { t.Fatalf("Assert Database: GetUser failed: %v", err) } @@ -288,7 +288,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { func Test_GetUsersHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT(logger) + jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) tests := []TestCase{ { @@ -359,7 +359,7 @@ func Test_GetUsersHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - user, err := user.GetUser(Handler.UserRepo, got.UserID) + user, err := Handler.UserService.GetUser(got.UserID) if err != nil { t.Fatalf("Assert Database: GetUser failed: %v", err) } @@ -378,7 +378,7 @@ func Test_GetUsersHandler_Integration(t *testing.T) { func Test_LoginHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, _ = testutil.CreateTestUserAndJWT(logger) + _, _ = testutil.CreateTestUserAndJWT(Handler.UserService, logger) tests := []TestCase{ { @@ -499,7 +499,7 @@ func Test_LoginHandler_Integration(t *testing.T) { func Test_RefreshTokensHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, userID := testutil.CreateTestUserAndJWT(logger) + _, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) refreshToken, err := token.GetStoredRefreshToken(Handler.TokenRepo, userID) if err != nil { t.Fatalf("TC GetStoredRefreshToken failed: %v", err) @@ -630,7 +630,7 @@ func Test_RefreshTokensHandler_Integration(t *testing.T) { func Test_InterviewsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT(logger) + jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) expiredJWT := testutil.CreateTestExpiredJWT(userID, -1, logger) tests := []TestCase{ @@ -768,7 +768,7 @@ func Test_InterviewsHandler_Integration(t *testing.T) { func Test_CreateConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT(logger) + jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, logger) mockAI.Scenario = mocks.ScenarioInterview interviewID := testutil.CreateTestInterview(jwtoken, logger) conversationsURL := testutil.TestServerURL + fmt.Sprintf("/api/conversations/create/%d", interviewID) @@ -878,7 +878,7 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { func Test_AppendConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT(logger) + jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, logger) mockAI.Scenario = mocks.ScenarioInterview interviewID := testutil.CreateTestInterview(jwtoken, logger) diff --git a/internal/testutil/helpers.go b/internal/testutil/helpers.go index 50587d6..98c3357 100644 --- a/internal/testutil/helpers.go +++ b/internal/testutil/helpers.go @@ -18,7 +18,7 @@ import ( "github.com/michaelboegner/interviewer/user" ) -func CreateTestUserAndJWT(logger *slog.Logger) (string, int) { +func CreateTestUserAndJWT(userService *user.UserService, logger *slog.Logger) (string, int) { var ( jwt string userID int @@ -28,7 +28,7 @@ func CreateTestUserAndJWT(logger *slog.Logger) (string, int) { email := "test@test.com" password := "test" - verificationJWT, err := user.VerificationToken(email, username, password) + verificationJWT, err := userService.VerificationToken(email, username, password) if err != nil { logger.Error("GenerateEmailVerificationToken failed", "error", err) } diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 84b61b0..a880953 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -41,12 +41,13 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { mailer := mocks.NewMockMailer() billing, err := billing.NewBilling(logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) + userService := user.NewUserService(userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) From 2838925ee436a9a3beaf02da25b29261c7b52849 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 15:16:47 +0700 Subject: [PATCH 08/32] fixed service_test.go with NewUserService and logger instantiations --- user/service_test.go | 47 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/user/service_test.go b/user/service_test.go index 3888198..277fe93 100644 --- a/user/service_test.go +++ b/user/service_test.go @@ -3,6 +3,7 @@ package user import ( "fmt" "log" + "log/slog" "os" "strings" "testing" @@ -49,17 +50,16 @@ func TestCreateUser(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + userRepo := NewMockRepo() + userService := NewUserService(userRepo, logger) + userRepo.failRepo = tc.failRepo - repo := NewMockRepo() - repo.failRepo = tc.failRepo - - jwt, err := VerificationToken(tc.email, tc.username, tc.password) + jwt, err := userService.VerificationToken(tc.email, tc.username, tc.password) if err != nil { t.Fatalf("VerificationToken failed: %v", err) } - user, err := CreateUser(repo, jwt) + user, err := userService.CreateUser(jwt) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -115,13 +115,12 @@ func TestLoginUser(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - - repo := NewMockRepo() - repo.failRepo = tc.failRepo + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + userRepo := NewMockRepo() + userService := NewUserService(userRepo, logger) + userRepo.failRepo = tc.failRepo - jwtoken, username, userID, err := LoginUser(repo, tc.email, tc.password) + jwtoken, username, userID, err := userService.LoginUser(tc.email, tc.password) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -178,13 +177,12 @@ func TestGetUser(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + userRepo := NewMockRepo() + userService := NewUserService(userRepo, logger) + userRepo.failRepo = tc.failRepo - repo := NewMockRepo() - repo.failRepo = tc.failRepo - - user, err := GetUser(repo, tc.userID) + user, err := userService.GetUser(tc.userID) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -231,13 +229,12 @@ func TestUpdateSubscription(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - - repo := NewMockRepo() - repo.failRepo = tc.failRepo + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + userRepo := NewMockRepo() + userService := NewUserService(userRepo, logger) + userRepo.failRepo = tc.failRepo - user, err := GetUser(repo, tc.userID) + user, err := userService.GetUser(tc.userID) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") From 68b43091d70e22f62b855ae17e93061c7cbc58dc Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 15:28:52 +0700 Subject: [PATCH 09/32] replaced user. calls with h.UserService calls --- handlers/handlers.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index ba25ab2..e4bb8fa 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -52,7 +52,7 @@ func (h *Handler) RequestVerificationHandler(w http.ResponseWriter, r *http.Requ return } - verificationJWT, err := user.VerificationToken(req.Email, req.Username, req.Password) + verificationJWT, err := h.UserService.VerificationToken(req.Email, req.Username, req.Password) if err != nil { h.Logger.Error("GenerateEmailVerificationToken failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to create token") @@ -93,7 +93,7 @@ func (h *Handler) CheckEmailHandler(w http.ResponseWriter, r *http.Request) { return } - err := user.GetUserByEmail(h.UserRepo, req.Email) + err := h.UserService.GetUserByEmail(req.Email) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithJSON(w, http.StatusOK, map[string]bool{"exists": false}) @@ -123,7 +123,7 @@ func (h *Handler) CreateUsersHandler(w http.ResponseWriter, r *http.Request) { return } - userCreated, err := user.CreateUser(h.UserRepo, req.Token) + userCreated, err := h.UserService.CreateUser(req.Token) if err != nil { h.Logger.Error("CreateUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") @@ -177,7 +177,7 @@ func (h *Handler) GetUsersHandler(w http.ResponseWriter, r *http.Request) { return } - userReturned, err := user.GetUser(h.UserRepo, userID) + userReturned, err := h.UserService.GetUser(userID) if err != nil { h.Logger.Error("GetUsers error", "error", err) return @@ -217,7 +217,7 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - userReturned, err := user.GetUser(h.UserRepo, userID) + userReturned, err := h.UserService.GetUser(userID) if err != nil { h.Logger.Error("GetUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to find user") @@ -231,7 +231,7 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - err = user.MarkUserDeleted(h.UserRepo, userID) + err = h.UserService.MarkUserDeleted(userID) if err != nil { h.Logger.Error("DeleteUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to delete user") @@ -276,7 +276,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { } - jwToken, username, userID, err := user.LoginUser(h.UserRepo, params.Email, params.Password) + jwToken, username, userID, err := h.UserService.LoginUser(params.Email, params.Password) if err != nil { h.Logger.Error("LoginUser error", "error", err) if errors.Is(err, user.ErrAccountDeleted) { @@ -407,7 +407,7 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := user.GetOrCreateByEmail(h.UserRepo, githubUser.Email, githubUser.Login) + user, err := h.UserService.GetOrCreateByEmail(githubUser.Email, githubUser.Login) if err != nil { RespondWithError(w, http.StatusInternalServerError, "User creation failed") return @@ -482,7 +482,7 @@ func (h *Handler) RefreshTokensHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := h.UserRepo.GetUser(params.UserID) + user, err := h.UserService.UserRepo.GetUser(params.UserID) if err != nil { h.Logger.Error("h.UserRepo.GetUser error", "error", err) RespondWithError(w, http.StatusUnauthorized, "Account deactivated") @@ -529,7 +529,7 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { return } - userReturned, err := user.GetUser(h.UserRepo, userID) + userReturned, err := h.UserService.GetUser(userID) if err != nil { h.Logger.Error("GetUser error", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to find user") @@ -889,7 +889,7 @@ func (h *Handler) RequestResetHandler(w http.ResponseWriter, r *http.Request) { return } - resetJWT, err := user.RequestPasswordReset(h.UserRepo, params.Email) + resetJWT, err := h.UserService.RequestPasswordReset(params.Email) if err != nil { h.Logger.Error("Error generating reset token for email", "error", err) w.WriteHeader(http.StatusOK) @@ -923,7 +923,7 @@ func (h *Handler) ResetPasswordHandler(w http.ResponseWriter, r *http.Request) { RespondWithError(w, http.StatusBadRequest, "Invalid request body") return } - err := user.ResetPassword(h.UserRepo, params.NewPassword, params.Token) + err := h.UserService.ResetPassword(params.NewPassword, params.Token) if err != nil { h.Logger.Error("ResetPasswordHandler failed", "error", err) RespondWithError(w, http.StatusUnauthorized, "Invalid or expired token") @@ -954,7 +954,7 @@ func (h *Handler) CreateCheckoutSessionHandler(w http.ResponseWriter, r *http.Re return } - user, err := user.GetUser(h.UserRepo, userID) + user, err := h.UserService.GetUser(userID) if err != nil { RespondWithError(w, http.StatusInternalServerError, "Could not find user") return @@ -1002,7 +1002,7 @@ func (h *Handler) CancelSubscriptionHandler(w http.ResponseWriter, r *http.Reque return } - userReturned, err := user.GetUser(h.UserRepo, userID) + userReturned, err := h.UserService.GetUser(userID) if err != nil { h.Logger.Error("GetUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not retrieve user") @@ -1031,7 +1031,7 @@ func (h *Handler) ResumeSubscriptionHandler(w http.ResponseWriter, r *http.Reque return } - userReturned, err := user.GetUser(h.UserRepo, userID) + userReturned, err := h.UserService.GetUser(userID) if err != nil { h.Logger.Error("GetUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not retrieve user") @@ -1067,7 +1067,7 @@ func (h *Handler) ChangePlanHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := user.GetUser(h.UserRepo, userID) + user, err := h.UserService.GetUser(userID) if err != nil { RespondWithError(w, http.StatusInternalServerError, "Could not find user") return @@ -1159,7 +1159,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.ApplyCredits(h.UserRepo, h.BillingRepo, orderAttrs.UserEmail, orderAttrs.FirstOrderItem.VariantID) + err = h.Billing.ApplyCredits(h.BillingRepo, orderAttrs.UserEmail, orderAttrs.FirstOrderItem.VariantID) if err != nil { h.Logger.Error("h.Billing.ApplyCredits failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") From 2abbdf1875f02f4a17a5d19ef0f6397def9b9521 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 15:36:55 +0700 Subject: [PATCH 10/32] fixed billing to use BillingService for BillingRepo and UserRepo --- billing/model.go | 8 ++++- billing/service.go | 68 +++++++++++++++++++-------------------- internal/server/server.go | 2 +- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/billing/model.go b/billing/model.go index 10f6bf0..cbd77e9 100644 --- a/billing/model.go +++ b/billing/model.go @@ -7,9 +7,13 @@ import ( "os" "strconv" "time" + + "github.com/michaelboegner/interviewer/user" ) type Billing struct { + BillingRepo BillingRepo + UserRepo user.UserRepo APIKey string VariantIDIndividual int VariantIDPro int @@ -102,7 +106,7 @@ type BillingRepo interface { MarkWebhookProcessed(id string, event string) error } -func NewBilling(logger *slog.Logger) (*Billing, error) { +func NewBilling(billingRepo BillingRepo, userRepo user.UserRepo, logger *slog.Logger) (*Billing, error) { individualID, err := strconv.Atoi(os.Getenv("LEMON_VARIANT_ID_INDIVIDUAL")) if err != nil { return nil, fmt.Errorf("invalid INDIVIDUAL ID: %w", err) @@ -116,6 +120,8 @@ func NewBilling(logger *slog.Logger) (*Billing, error) { return nil, fmt.Errorf("invalid PREMIUM ID: %w", err) } return &Billing{ + BillingRepo: billingRepo, + UserRepo: userRepo, APIKey: os.Getenv("LEMON_API_KEY"), VariantIDIndividual: individualID, VariantIDPro: proID, diff --git a/billing/service.go b/billing/service.go index 7f32790..e314973 100644 --- a/billing/service.go +++ b/billing/service.go @@ -12,8 +12,6 @@ import ( "os" "strconv" "time" - - "github.com/michaelboegner/interviewer/user" ) func (b *Billing) RequestCheckoutSession(userEmail string, variantID int) (string, error) { @@ -186,8 +184,8 @@ func (b *Billing) VerifyBillingSignature(signature string, body []byte, secret s return hmac.Equal([]byte(expected), []byte(signature)) } -func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, email string, variantID int) error { - user, err := userRepo.GetUserByEmail(email) +func (b *Billing) ApplyCredits(email string, variantID int) error { + user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -216,7 +214,7 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, return fmt.Errorf("unknown variant ID: %d", variantID) } - if err := userRepo.AddCredits(user.ID, credits, creditType); err != nil { + if err := b.UserRepo.AddCredits(user.ID, credits, creditType); err != nil { b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -227,7 +225,7 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, CreditType: creditType, Reason: reason, } - if err := billingRepo.LogCreditTransaction(tx); err != nil { + if err := b.BillingRepo.LogCreditTransaction(tx); err != nil { b.Logger.Error("Warning: credit granted but failed to log transaction", "error", err) return err } @@ -235,8 +233,8 @@ func (b *Billing) ApplyCredits(userRepo user.UserRepo, billingRepo BillingRepo, return nil } -func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, orderAttrs OrderAttributes) error { - user, err := userRepo.GetUserByEmail(orderAttrs.UserEmail) +func (b *Billing) DeductCredits(orderAttrs OrderAttributes) error { + user, err := b.UserRepo.GetUserByEmail(orderAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -267,7 +265,7 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, return fmt.Errorf("unknown variant ID: %d", variantID) } - if err := userRepo.AddCredits(user.ID, -credits, creditType); err != nil { + if err := b.UserRepo.AddCredits(user.ID, -credits, creditType); err != nil { b.Logger.Error("repo.DeductCredits failed", "error", err) return err } @@ -278,7 +276,7 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, CreditType: creditType, Reason: reason, } - if err := billingRepo.LogCreditTransaction(tx); err != nil { + if err := b.BillingRepo.LogCreditTransaction(tx); err != nil { b.Logger.Warn("Refund deduction succeeded but failed to log transaction", "error", err) return err } @@ -286,8 +284,8 @@ func (b *Billing) DeductCredits(userRepo user.UserRepo, billingRepo BillingRepo, return nil } -func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs SubscriptionAttributes, subscriptionID string) error { - user, err := userRepo.GetUserByEmail(subCreatedAttrs.UserEmail) +func (b *Billing) CreateSubscription(subCreatedAttrs SubscriptionAttributes, subscriptionID string) error { + user, err := b.UserRepo.GetUserByEmail(subCreatedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -304,7 +302,7 @@ func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs Sub return fmt.Errorf("unknown variant ID: %d", subCreatedAttrs.VariantID) } - err = userRepo.UpdateSubscriptionData( + err = b.UserRepo.UpdateSubscriptionData( user.ID, "active", tier, @@ -320,14 +318,14 @@ func (b *Billing) CreateSubscription(userRepo user.UserRepo, subCreatedAttrs Sub return nil } -func (b *Billing) CancelSubscription(userRepo user.UserRepo, email string) error { - user, err := userRepo.GetUserByEmail(email) +func (b *Billing) CancelSubscription(email string) error { + user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } - err = userRepo.UpdateSubscriptionStatusData( + err = b.UserRepo.UpdateSubscriptionStatusData( user.ID, "cancelled", ) @@ -339,14 +337,14 @@ func (b *Billing) CancelSubscription(userRepo user.UserRepo, email string) error return nil } -func (b *Billing) ResumeSubscription(userRepo user.UserRepo, email string) error { - user, err := userRepo.GetUserByEmail(email) +func (b *Billing) ResumeSubscription(email string) error { + user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } - err = userRepo.UpdateSubscriptionStatusData( + err = b.UserRepo.UpdateSubscriptionStatusData( user.ID, "active", ) @@ -358,14 +356,14 @@ func (b *Billing) ResumeSubscription(userRepo user.UserRepo, email string) error return nil } -func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo BillingRepo, email string) error { - user, err := userRepo.GetUserByEmail(email) +func (b *Billing) ExpireSubscription(email string) error { + user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err } - err = userRepo.UpdateSubscriptionStatusData( + err = b.UserRepo.UpdateSubscriptionStatusData( user.ID, "expired", ) @@ -375,7 +373,7 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing } if user.SubscriptionCredits > 0 { - err = userRepo.AddCredits(user.ID, -user.SubscriptionCredits, "subscription") + err = b.UserRepo.AddCredits(user.ID, -user.SubscriptionCredits, "subscription") if err != nil { b.Logger.Error("repo.AddCredits failed", "error", err) return err @@ -387,7 +385,7 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing CreditType: "subscription", Reason: "Zeroed out credits on subscription expiration", } - if err := billingRepo.LogCreditTransaction(tx); err != nil { + if err := b.BillingRepo.LogCreditTransaction(tx); err != nil { b.Logger.Warn("Zero-out succeeded but failed to log transaction", "error", err) } } @@ -395,8 +393,8 @@ func (b *Billing) ExpireSubscription(userRepo user.UserRepo, billingRepo Billing return nil } -func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingRepo, subRenewAttrs SubscriptionRenewAttributes) error { - user, err := userRepo.GetUserByEmail(subRenewAttrs.UserEmail) +func (b *Billing) RenewSubscription(subRenewAttrs SubscriptionRenewAttributes) error { + user, err := b.UserRepo.GetUserByEmail(subRenewAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -422,7 +420,7 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR return fmt.Errorf("unknown user.SubscriptionTier: %s", user.SubscriptionTier) } - if err := userRepo.AddCredits(user.ID, credits, "subscription"); err != nil { + if err := b.UserRepo.AddCredits(user.ID, credits, "subscription"); err != nil { b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -433,7 +431,7 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR CreditType: "subscription", Reason: reason, } - if err := billingRepo.LogCreditTransaction(tx); err != nil { + if err := b.BillingRepo.LogCreditTransaction(tx); err != nil { b.Logger.Warn("credit granted but failed to log transaction", "error", err) return err } @@ -441,8 +439,8 @@ func (b *Billing) RenewSubscription(userRepo user.UserRepo, billingRepo BillingR return nil } -func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo BillingRepo, subChangedAttrs SubscriptionAttributes) error { - user, err := userRepo.GetUserByEmail(subChangedAttrs.UserEmail) +func (b *Billing) ChangeSubscription(subChangedAttrs SubscriptionAttributes) error { + user, err := b.UserRepo.GetUserByEmail(subChangedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -471,7 +469,7 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing return fmt.Errorf("unknown user.SubscriptionTier: %s", user.SubscriptionTier) } - if err := userRepo.AddCredits(user.ID, credits, "subscription"); err != nil { + if err := b.UserRepo.AddCredits(user.ID, credits, "subscription"); err != nil { b.Logger.Error("repo.AddCredits failed", "error", err) return err } @@ -482,7 +480,7 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing CreditType: "subscription", Reason: reason, } - if err := billingRepo.LogCreditTransaction(tx); err != nil { + if err := b.BillingRepo.LogCreditTransaction(tx); err != nil { b.Logger.Warn("credit granted but failed to log transaction", "error", err) return err } @@ -490,8 +488,8 @@ func (b *Billing) ChangeSubscription(userRepo user.UserRepo, billingRepo Billing return nil } -func (b *Billing) UpdateSubscription(userRepo user.UserRepo, subUpdatedAttrs SubscriptionAttributes, subscriptionID string) error { - user, err := userRepo.GetUserByEmail(subUpdatedAttrs.UserEmail) +func (b *Billing) UpdateSubscription(subUpdatedAttrs SubscriptionAttributes, subscriptionID string) error { + user, err := b.UserRepo.GetUserByEmail(subUpdatedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) return err @@ -508,7 +506,7 @@ func (b *Billing) UpdateSubscription(userRepo user.UserRepo, subUpdatedAttrs Sub return fmt.Errorf("unknown variant ID: %d", subUpdatedAttrs.VariantID) } - err = userRepo.UpdateSubscriptionData( + err = b.UserRepo.UpdateSubscriptionData( user.ID, subUpdatedAttrs.Status, tier, diff --git a/internal/server/server.go b/internal/server/server.go index 8658cf0..e96b242 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -39,7 +39,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) userService := user.NewUserService(userRepo, logger) mailer := mailer.NewMailer(logger) - billing, err := billing.NewBilling(logger) + billing, err := billing.NewBilling(billingRepo, userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err From d0f6c98f66cd426aa42c6cb05cfaecd2b5eb3b52 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 15:38:09 +0700 Subject: [PATCH 11/32] fixed billing.service_test.go --- billing/service_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/billing/service_test.go b/billing/service_test.go index 07b13b9..3023319 100644 --- a/billing/service_test.go +++ b/billing/service_test.go @@ -102,7 +102,7 @@ func TestApplyCredits(t *testing.T) { b := NewTestBilling() - err := b.ApplyCredits(userRepo, billingRepo, "test@example.com", tc.variantID) + err := b.ApplyCredits("test@example.com", tc.variantID) if tc.expectErr && err == nil { t.Fatal("expected error but got nil") } @@ -188,7 +188,7 @@ func TestDeductCredits(t *testing.T) { }, } - err := b.DeductCredits(userRepo, billingRepo, attrs) + err := b.DeductCredits(attrs) if tc.expectErr && err == nil { t.Fatal("expected error but got nil") } From caa9c850197d5412514b5f0de01c2a83d3a2624e Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 15:49:46 +0700 Subject: [PATCH 12/32] fixed test server billing.newBilling to include billingRepo and userRepo --- internal/testutil/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index a880953..580ed40 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -39,7 +39,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { billingRepo := billing.NewRepository(db) openAI := mocks.NewMockOpenAIClient() mailer := mocks.NewMockMailer() - billing, err := billing.NewBilling(logger) + billing, err := billing.NewBilling(billingRepo, userRepo, logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) userService := user.NewUserService(userRepo, logger) if err != nil { From 9666c8818b86c49bb4bbaded4ba3f54431b9bde5 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 16:06:59 +0700 Subject: [PATCH 13/32] converted token service to TokenService struct and methods --- handlers/model.go | 9 +++------ internal/server/server.go | 3 ++- internal/testutil/server.go | 3 ++- token/model.go | 13 +++++++++++++ token/service.go | 29 ++++++++++++++++------------- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/handlers/model.go b/handlers/model.go index 1d30827..8010e7e 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -56,8 +56,7 @@ type Handler struct { UserService *user.UserService InterviewService *interview.InterviewService ConversationRepo conversation.ConversationRepo - TokenRepo token.TokenRepo - BillingRepo billing.BillingRepo + TokenService *token.TokenService Billing *billing.Billing Mailer mailer.MailerClient OpenAI chatgpt.AIClient @@ -68,9 +67,8 @@ type Handler struct { func NewHandler( interviewService *interview.InterviewService, userService *user.UserService, - tokenRepo token.TokenRepo, + tokenService *token.TokenService, conversationRepo conversation.ConversationRepo, - billingRepo billing.BillingRepo, billing *billing.Billing, mailer mailer.MailerClient, openAI chatgpt.AIClient, @@ -79,9 +77,8 @@ func NewHandler( return &Handler{ InterviewService: interviewService, UserService: userService, - TokenRepo: tokenRepo, + TokenService: tokenService, ConversationRepo: conversationRepo, - BillingRepo: billingRepo, Billing: billing, Mailer: mailer, OpenAI: openAI, diff --git a/internal/server/server.go b/internal/server/server.go index e96b242..d14ddff 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -40,12 +40,13 @@ func NewServer(logger *slog.Logger) (*Server, error) { userService := user.NewUserService(userRepo, logger) mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(billingRepo, userRepo, logger) + tokenService := token.NewTokenService(tokenRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billing, mailer, openAI, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 580ed40..bd230c8 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -42,12 +42,13 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { billing, err := billing.NewBilling(billingRepo, userRepo, logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) userService := user.NewUserService(userRepo, logger) + tokenSerice := token.NewTokenService(tokenRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billing, mailer, openAI, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) diff --git a/token/model.go b/token/model.go index 4cd8ad8..90d7b71 100644 --- a/token/model.go +++ b/token/model.go @@ -1,11 +1,17 @@ package token import ( + "log/slog" "time" "github.com/golang-jwt/jwt/v5" ) +type TokenService struct { + TokenRepo TokenRepo + Logger *slog.Logger +} + type RefreshToken struct { UserID int RefreshToken string @@ -14,6 +20,13 @@ type RefreshToken struct { UpdatedAt time.Time } +func NewTokenService(tokenRepo TokenRepo, logger *slog.Logger) *TokenService { + return &TokenService{ + TokenRepo: tokenRepo, + Logger: logger, + } +} + type CustomClaims struct { UserID string `json:"sub"` jwt.RegisteredClaims diff --git a/token/service.go b/token/service.go index f1c07f5..f8f3b44 100644 --- a/token/service.go +++ b/token/service.go @@ -5,7 +5,6 @@ import ( "crypto/subtle" "encoding/hex" "fmt" - "log" "os" "strconv" "time" @@ -13,7 +12,7 @@ import ( "github.com/golang-jwt/jwt/v5" ) -func CreateJWT(subject string, expires int) (string, error) { +func (t *TokenService) CreateJWT(subject string, expires int) (string, error) { var ( key []byte jwtoken *jwt.Token @@ -35,20 +34,21 @@ func CreateJWT(subject string, expires int) (string, error) { jwtoken = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := jwtoken.SignedString(key) if err != nil { - log.Fatalf("Bad SignedString: %s", err) + t.Logger.Error("Bad SignedString", "error", err) return "", err } return tokenString, nil } -func CreateRefreshToken(repo TokenRepo, userID int) (string, error) { +func (t *TokenService) CreateRefreshToken(userID int) (string, error) { now := time.Now().UTC() refreshLength := 32 refreshBytes := make([]byte, refreshLength) _, err := rand.Read([]byte(refreshBytes)) if err != nil { + t.Logger.Error("rand.Read failed", "error", err) return "", err } token := hex.EncodeToString(refreshBytes) @@ -62,37 +62,39 @@ func CreateRefreshToken(repo TokenRepo, userID int) (string, error) { UpdatedAt: now, } - err = repo.AddRefreshToken(refreshToken) + err = t.TokenRepo.AddRefreshToken(refreshToken) if err != nil { + t.Logger.Error("t.TokenRepo.AddRefreshToken failed", "error", err) return "", err } return refreshToken.RefreshToken, nil } -func DeleteRefreshToken(repo TokenRepo, userID int) error { - err := repo.DeleteRefreshToken(userID) +func (t *TokenService) DeleteRefreshToken(userID int) error { + err := t.TokenRepo.DeleteRefreshToken(userID) if err != nil { - log.Printf("repo.DeleteRefreshToken failed: %v", err) + t.Logger.Error("t.TokenRepo.DeleteRefreshToken failed", "error", err) return err } return nil } -func GetStoredRefreshToken(repo TokenRepo, userID int) (string, error) { - storedToken, err := repo.GetStoredRefreshToken(userID) +func (t *TokenService) GetStoredRefreshToken(userID int) (string, error) { + storedToken, err := t.TokenRepo.GetStoredRefreshToken(userID) if err != nil { + t.Logger.Error("t.TokenRepo.GetStoredRefreshToken failed", "error", err) return "", err } return storedToken, nil } -func VerifyRefreshToken(storedToken, providedToken string) bool { +func (t *TokenService) VerifyRefreshToken(storedToken, providedToken string) bool { return subtle.ConstantTimeCompare([]byte(storedToken), []byte(providedToken)) == 1 } -func ExtractUserIDFromToken(tokenString string) (int, error) { +func (t *TokenService) ExtractUserIDFromToken(tokenString string) (int, error) { jwtSecret := os.Getenv("JWT_SECRET") token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(tokenString *jwt.Token) (interface{}, error) { @@ -102,7 +104,7 @@ func ExtractUserIDFromToken(tokenString string) (int, error) { return []byte(jwtSecret), nil }) if err != nil { - log.Printf("ParseWithClaims failed: %v", err) + t.Logger.Error("jwt.ParseWithClaims failed", "error", err) return 0, err } @@ -110,6 +112,7 @@ func ExtractUserIDFromToken(tokenString string) (int, error) { if ok && token.Valid { userID, err := strconv.Atoi(claims.UserID) if err != nil { + t.Logger.Error("strconv.Atoi failed", "error", err) return 0, err } From c18795a9a9d42f8c11575e98fd1651527792d2f7 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Wed, 8 Oct 2025 16:28:01 +0700 Subject: [PATCH 14/32] converted token service to TokenService struct with methods --- handlers/handlers_test.go | 14 +++++----- internal/testutil/helpers.go | 4 +-- token/service_test.go | 53 ++++++++++++++---------------------- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 90776e8..03ff867 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -288,7 +288,7 @@ func Test_CreateUsersHandler_Integration(t *testing.T) { func Test_GetUsersHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) + jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) tests := []TestCase{ { @@ -378,7 +378,7 @@ func Test_GetUsersHandler_Integration(t *testing.T) { func Test_LoginHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, _ = testutil.CreateTestUserAndJWT(Handler.UserService, logger) + _, _ = testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) tests := []TestCase{ { @@ -499,8 +499,8 @@ func Test_LoginHandler_Integration(t *testing.T) { func Test_RefreshTokensHandler_Integration(t *testing.T) { cleanDBOrFail(t) - _, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) - refreshToken, err := token.GetStoredRefreshToken(Handler.TokenRepo, userID) + _, userID := testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) + refreshToken, err := Handler.TokenService.GetStoredRefreshToken(userID) if err != nil { t.Fatalf("TC GetStoredRefreshToken failed: %v", err) } @@ -630,7 +630,7 @@ func Test_RefreshTokensHandler_Integration(t *testing.T) { func Test_InterviewsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, logger) + jwtoken, userID := testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) expiredJWT := testutil.CreateTestExpiredJWT(userID, -1, logger) tests := []TestCase{ @@ -768,7 +768,7 @@ func Test_InterviewsHandler_Integration(t *testing.T) { func Test_CreateConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, logger) + jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) mockAI.Scenario = mocks.ScenarioInterview interviewID := testutil.CreateTestInterview(jwtoken, logger) conversationsURL := testutil.TestServerURL + fmt.Sprintf("/api/conversations/create/%d", interviewID) @@ -878,7 +878,7 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { func Test_AppendConversationsHandler_Integration(t *testing.T) { cleanDBOrFail(t) - jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, logger) + jwtoken, _ := testutil.CreateTestUserAndJWT(Handler.UserService, Handler.TokenService, logger) mockAI.Scenario = mocks.ScenarioInterview interviewID := testutil.CreateTestInterview(jwtoken, logger) diff --git a/internal/testutil/helpers.go b/internal/testutil/helpers.go index 98c3357..ecde95f 100644 --- a/internal/testutil/helpers.go +++ b/internal/testutil/helpers.go @@ -18,7 +18,7 @@ import ( "github.com/michaelboegner/interviewer/user" ) -func CreateTestUserAndJWT(userService *user.UserService, logger *slog.Logger) (string, int) { +func CreateTestUserAndJWT(userService *user.UserService, tokenService *token.TokenService, logger *slog.Logger) (string, int) { var ( jwt string userID int @@ -61,7 +61,7 @@ func CreateTestUserAndJWT(userService *user.UserService, logger *slog.Logger) (s jwt = returnVals.JWToken //test userID extract - userID, err = token.ExtractUserIDFromToken(jwt) + userID, err = tokenService.ExtractUserIDFromToken(jwt) if err != nil { logger.Error("CreateTestUserandJWT userID extraction failed", "error", err) } diff --git a/token/service_test.go b/token/service_test.go index e202a6c..fcc8db2 100644 --- a/token/service_test.go +++ b/token/service_test.go @@ -1,8 +1,7 @@ package token import ( - "fmt" - "log" + "log/slog" "os" "strconv" "strings" @@ -34,15 +33,12 @@ func TestCreateRefreshToken(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + tokenRepo := NewMockRepo() + tokenService := NewTokenService(tokenRepo, logger) + tokenRepo.failRepo = tc.failRepo - repo := NewMockRepo() - if tc.failRepo { - repo.failRepo = true - } - - token, err := CreateRefreshToken(repo, tc.userID) + token, err := tokenService.CreateRefreshToken(tc.userID) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -88,15 +84,12 @@ func TestGetStoredRefreshToken(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - - repo := NewMockRepo() - if tc.failRepo { - repo.failRepo = true - } + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + tokenRepo := NewMockRepo() + tokenService := NewTokenService(tokenRepo, logger) + tokenRepo.failRepo = tc.failRepo - token, err := GetStoredRefreshToken(repo, tc.userID) + token, err := tokenService.GetStoredRefreshToken(tc.userID) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -141,10 +134,11 @@ func TestVerifyRefreshToken(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + tokenRepo := NewMockRepo() + tokenService := NewTokenService(tokenRepo, logger) - result := VerifyRefreshToken(tc.storedToken, tc.inputToken) + result := tokenService.VerifyRefreshToken(tc.storedToken, tc.inputToken) if result != tc.expected { t.Errorf("expected %v but got %v", tc.expected, result) @@ -178,20 +172,20 @@ func TestExtractUserIDFromToken(t *testing.T) { var buf strings.Builder var token string var err error - - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + tokenRepo := NewMockRepo() + tokenService := NewTokenService(tokenRepo, logger) if tc.invalid { token = "invalid.token.value" } else { - token, err = CreateJWT(strconv.Itoa(tc.userID), 3600) + token, err = tokenService.CreateJWT(strconv.Itoa(tc.userID), 3600) if err != nil { t.Fatalf("failed to create JWT: %v", err) } } - uid, err := ExtractUserIDFromToken(token) + uid, err := tokenService.ExtractUserIDFromToken(token) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -211,10 +205,3 @@ func TestExtractUserIDFromToken(t *testing.T) { }) } } - -func showLogsIfFail(t *testing.T, name string, buf strings.Builder) { - log.SetOutput(os.Stderr) - if t.Failed() { - fmt.Printf("---- logs for test: %s ----\n%s\n", name, buf.String()) - } -} From 567d69d2e7ca79bce8ea10ea159b08a9f2957c18 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Thu, 9 Oct 2025 13:28:18 +0700 Subject: [PATCH 15/32] added TokenService to handlers and handlers_test --- handlers/handlers.go | 25 ++++++++++++------------- handlers/handlers_test.go | 5 ++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index e4bb8fa..d5ed148 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -17,7 +17,6 @@ import ( "github.com/michaelboegner/interviewer/dashboard" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/middleware" - "github.com/michaelboegner/interviewer/token" "github.com/michaelboegner/interviewer/user" ) @@ -130,9 +129,9 @@ func (h *Handler) CreateUsersHandler(w http.ResponseWriter, r *http.Request) { return } - jwt, err := token.CreateJWT(strconv.Itoa(userCreated.ID), 0) + jwt, err := h.TokenService.CreateJWT(strconv.Itoa(userCreated.ID), 0) if err != nil { - h.Logger.Error("token.CreateJWT failed", "error", err) + h.Logger.Error("h.TokenService.CreateJWT failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -238,7 +237,7 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - err = token.DeleteRefreshToken(h.TokenRepo, userID) + err = h.TokenService.DeleteRefreshToken(userID) if err != nil { h.Logger.Error("DeleteRefreshTokensForUser failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") @@ -287,7 +286,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { return } - refreshToken, err := token.CreateRefreshToken(h.TokenRepo, userID) + refreshToken, err := h.TokenService.CreateRefreshToken(userID) if err != nil { h.Logger.Error("RefreshToken error", "error", err) RespondWithError(w, http.StatusUnauthorized, "") @@ -413,15 +412,15 @@ func (h *Handler) GithubLoginHandler(w http.ResponseWriter, r *http.Request) { return } - jwt, err := token.CreateJWT(strconv.Itoa(user.ID), 0) + jwt, err := h.TokenService.CreateJWT(strconv.Itoa(user.ID), 0) if err != nil { - h.Logger.Error("token.CreateJWT failed", "error", err) + h.Logger.Error("h.TokenService.CreateJWT failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } - refreshToken, err := token.CreateRefreshToken(h.TokenRepo, user.ID) + refreshToken, err := h.TokenService.CreateRefreshToken(user.ID) if err != nil { - h.Logger.Error("token.CreateRefreshToken failed", "error", err) + h.Logger.Error("h.TokenService.CreateRefreshToken failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") return } @@ -461,21 +460,21 @@ func (h *Handler) RefreshTokensHandler(w http.ResponseWriter, r *http.Request) { return } - storedToken, err := token.GetStoredRefreshToken(h.TokenRepo, params.UserID) + storedToken, err := h.TokenService.GetStoredRefreshToken(params.UserID) if err != nil { h.Logger.Error("GetStoredRefreshToken error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid user_id") return } - ok := token.VerifyRefreshToken(storedToken, providedToken) + ok := h.TokenService.VerifyRefreshToken(storedToken, providedToken) if !ok { h.Logger.Error("VerifyRefreshToken error") RespondWithError(w, http.StatusUnauthorized, "Refresh token is invalid") return } - refreshToken, err := token.CreateRefreshToken(h.TokenRepo, params.UserID) + refreshToken, err := h.TokenService.CreateRefreshToken(params.UserID) if err != nil { h.Logger.Error("CreateRefreshToken error", "error", err) RespondWithError(w, http.StatusUnauthorized, "") @@ -494,7 +493,7 @@ func (h *Handler) RefreshTokensHandler(w http.ResponseWriter, r *http.Request) { return } - jwToken, err := token.CreateJWT(strconv.Itoa(params.UserID), 0) + jwToken, err := h.TokenService.CreateJWT(strconv.Itoa(params.UserID), 0) if err != nil { h.Logger.Error("JWT creation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "") diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 03ff867..cf140c0 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -19,7 +19,6 @@ import ( "github.com/michaelboegner/interviewer/internal/mocks" "github.com/michaelboegner/interviewer/internal/testutil" "github.com/michaelboegner/interviewer/interview" - "github.com/michaelboegner/interviewer/token" "github.com/michaelboegner/interviewer/user" ) @@ -480,7 +479,7 @@ func Test_LoginHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - refreshToken, err := token.GetStoredRefreshToken(Handler.TokenRepo, respUnmarshalled.UserID) + refreshToken, err := Handler.TokenService.GetStoredRefreshToken(respUnmarshalled.UserID) if err != nil { t.Fatalf("Assert Database: GetUser failed: %v", err) } @@ -611,7 +610,7 @@ func Test_RefreshTokensHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - refreshToken, err := token.GetStoredRefreshToken(Handler.TokenRepo, userID) + refreshToken, err := Handler.TokenService.GetStoredRefreshToken(userID) if err != nil { t.Fatalf("Assert Database: GetUser failed: %v", err) } From bcde33883bb8052cb339c4fe51e2bc4589d159a3 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sat, 11 Oct 2025 16:25:31 +0700 Subject: [PATCH 16/32] extracted token.CreateJWT from user service to be orchestrated in handler --- handlers/handlers.go | 17 +++++++++++++++-- user/service.go | 36 ++++++------------------------------ user/service_test.go | 21 ++++++++++++++++++--- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index d5ed148..381cc92 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "log" "net/http" "net/url" "os" @@ -275,7 +276,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { } - jwToken, username, userID, err := h.UserService.LoginUser(params.Email, params.Password) + username, userID, err := h.UserService.LoginUser(params.Email, params.Password) if err != nil { h.Logger.Error("LoginUser error", "error", err) if errors.Is(err, user.ErrAccountDeleted) { @@ -286,6 +287,12 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { return } + jwToken, err := h.TokenService.CreateJWT(strconv.Itoa(userID), 0) + if err != nil { + log.Printf("JWT creation failed: %v", err) + return "", "", 0, err + } + refreshToken, err := h.TokenService.CreateRefreshToken(userID) if err != nil { h.Logger.Error("RefreshToken error", "error", err) @@ -888,7 +895,13 @@ func (h *Handler) RequestResetHandler(w http.ResponseWriter, r *http.Request) { return } - resetJWT, err := h.UserService.RequestPasswordReset(params.Email) + err = h.UserService.GetUserByEmail(params.Email) + if err != nil { + w.WriteHeader(http.StatusOK) + return + } + + resetJWT, err := h.TokenService.CreateJWT(params.Email, 900) if err != nil { h.Logger.Error("Error generating reset token for email", "error", err) w.WriteHeader(http.StatusOK) diff --git a/user/service.go b/user/service.go index d273afa..bb3dd10 100644 --- a/user/service.go +++ b/user/service.go @@ -4,11 +4,9 @@ import ( "errors" "log" "os" - "strconv" "time" "github.com/golang-jwt/jwt/v5" - "github.com/michaelboegner/interviewer/token" "golang.org/x/crypto/bcrypt" ) @@ -60,34 +58,28 @@ func (u *UserService) CreateUser(tokenStr string) (*User, error) { return user, nil } -func (u *UserService) LoginUser(email, password string) (string, string, int, error) { +func (u *UserService) LoginUser(email, password string) (string, int, error) { userID, hashedPassword, err := u.UserRepo.GetPasswordandID(email) if err != nil { - return "", "", 0, err + return "", 0, err } user, err := u.UserRepo.GetUser(userID) if err != nil { log.Printf("u.UserRepo.GetUser failed: %v", err) - return "", "", 0, err + return "", 0, err } if user.AccountStatus != "active" { - return "", "", 0, ErrAccountDeleted + return "", 0, ErrAccountDeleted } err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) if err != nil { - return "", "", 0, err + return "", 0, err } - jwToken, err := token.CreateJWT(strconv.Itoa(userID), 0) - if err != nil { - log.Printf("JWT creation failed: %v", err) - return "", "", 0, err - } - - return jwToken, user.Username, userID, nil + return user.Username, userID, nil } func (u *UserService) GetUser(userID int) (*User, error) { @@ -119,22 +111,6 @@ func (u *UserService) GetUserByEmail(email string) error { return nil } -func (u *UserService) RequestPasswordReset(email string) (string, error) { - user, err := u.UserRepo.GetUserByEmail(email) - if err != nil { - log.Printf("GetUserByEmail failed: %v", err) - return "", err - } - - resetJWT, err := token.CreateJWT(user.Email, 900) - if err != nil { - log.Printf("CreateJWT failed: %v", err) - return "", err - } - - return resetJWT, nil -} - func (u *UserService) ResetPassword(newPassword string, resetJWT string) error { email, err := verifyResetToken(resetJWT) if err != nil { diff --git a/user/service_test.go b/user/service_test.go index 277fe93..146181e 100644 --- a/user/service_test.go +++ b/user/service_test.go @@ -5,11 +5,13 @@ import ( "log" "log/slog" "os" + "strconv" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/michaelboegner/interviewer/token" "golang.org/x/crypto/bcrypt" ) @@ -114,13 +116,26 @@ func TestLoginUser(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder + var ( + buf strings.Builder + jwToken string + ) logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) userRepo := NewMockRepo() + tokenRepo := token.NewMockRepo() userService := NewUserService(userRepo, logger) + tokenService := token.NewTokenService(tokenRepo, logger) userRepo.failRepo = tc.failRepo - jwtoken, username, userID, err := userService.LoginUser(tc.email, tc.password) + username, userID, err := userService.LoginUser(tc.email, tc.password) + if err != nil { + t.Fatalf("userService.LoginUser failed: %v", err) + } + + jwToken, err = tokenService.CreateJWT(strconv.Itoa(userID), 0) + if err != nil { + t.Fatalf("JWT creation failed: %v", err) + } if tc.expectError && err == nil { t.Fatalf("expected error but got nil") @@ -136,7 +151,7 @@ func TestLoginUser(t *testing.T) { if diff := cmp.Diff(expected, got); diff != "" { t.Errorf("User mismatch (-want +got):\n%s", diff) } - if jwtoken == "" { + if jwToken == "" { t.Errorf("Expected jwtoken but got empty string") } if username == "" { From 122bfb5173d5e0628257be7261f4e6a1a8775b55 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sun, 12 Oct 2025 16:02:59 +0700 Subject: [PATCH 17/32] aligned OpenAI service with service naming being used in rest of services --- chatgpt/model.go | 6 +++--- chatgpt/service.go | 10 +++++----- conversation/service_test.go | 4 ++-- handlers/handlers_test.go | 4 ++-- internal/mocks/openai_mock.go | 18 +++++++++--------- internal/server/server.go | 4 ++-- internal/testutil/server.go | 2 +- interview/service_test.go | 8 ++++---- learninglog/03_29_2025.md | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/chatgpt/model.go b/chatgpt/model.go index 9a1c81b..a7d3ccd 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -23,7 +23,7 @@ type ChatGPTResponse struct { Level string `json:"level"` } -type OpenAIClient struct { +type OpenAIService struct { APIKey string Logger *slog.Logger } @@ -45,8 +45,8 @@ func (e *OpenAIError) Error() string { return fmt.Sprintf("OpenAI error %d: %s", e.StatusCode, e.Message) } -func NewOpenAI(logger *slog.Logger) *OpenAIClient { - return &OpenAIClient{ +func NewOpenAIService(logger *slog.Logger) *OpenAIService { + return &OpenAIService{ APIKey: os.Getenv("OPENAI_API_KEY"), Logger: logger, } diff --git a/chatgpt/service.go b/chatgpt/service.go index 03c6d19..b129b26 100644 --- a/chatgpt/service.go +++ b/chatgpt/service.go @@ -11,7 +11,7 @@ import ( "strings" ) -func (c *OpenAIClient) GetChatGPTResponse(prompt string) (*ChatGPTResponse, error) { +func (c *OpenAIService) GetChatGPTResponse(prompt string) (*ChatGPTResponse, error) { ctx := context.Background() var messagesArray []map[string]string @@ -84,7 +84,7 @@ func (c *OpenAIClient) GetChatGPTResponse(prompt string) (*ChatGPTResponse, erro return &chatGPTResponse, nil } -func (c *OpenAIClient) GetChatGPTResponseConversation(conversationHistory []map[string]string) (*ChatGPTResponse, error) { +func (c *OpenAIService) GetChatGPTResponseConversation(conversationHistory []map[string]string) (*ChatGPTResponse, error) { ctx := context.Background() requestBody, err := json.Marshal(map[string]interface{}{ @@ -151,7 +151,7 @@ func (c *OpenAIClient) GetChatGPTResponseConversation(conversationHistory []map[ return &chatGPTResponse, nil } -func (c *OpenAIClient) GetChatGPT35Response(prompt string) (*ChatGPTResponse, error) { +func (c *OpenAIService) GetChatGPT35Response(prompt string) (*ChatGPTResponse, error) { ctx := context.Background() var messagesArray []map[string]string @@ -224,7 +224,7 @@ func (c *OpenAIClient) GetChatGPT35Response(prompt string) (*ChatGPTResponse, er return &chatGPTResponse, nil } -func (c *OpenAIClient) ExtractJDInput(jd string) (*JDParsedOutput, error) { +func (c *OpenAIService) ExtractJDInput(jd string) (*JDParsedOutput, error) { systemPrompt := BuildJDPromptInput(jd) response, err := c.GetChatGPT35Response(systemPrompt) if err != nil { @@ -239,7 +239,7 @@ func (c *OpenAIClient) ExtractJDInput(jd string) (*JDParsedOutput, error) { }, nil } -func (c *OpenAIClient) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) { +func (c *OpenAIService) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) { jdJSON, err := json.MarshalIndent(jdInput, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal JDParsedOutput: %w", err) diff --git a/conversation/service_test.go b/conversation/service_test.go index b6ebc13..4717753 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -15,7 +15,7 @@ import ( ) func TestCreateConversation(t *testing.T) { - ai := &mocks.MockOpenAIClient{} + ai := &mocks.MockOpenAIService{} tests := []struct { name string @@ -126,7 +126,7 @@ func TestCreateConversation(t *testing.T) { } func TestAppendConversation(t *testing.T) { - ai := &mocks.MockOpenAIClient{} + ai := &mocks.MockOpenAIService{} tests := []struct { name string diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index cf140c0..247a2f1 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -46,7 +46,7 @@ type TestCase struct { var ( Handler *handlers.Handler conversationBuilder *testutil.ConversationBuilder - mockAI *mocks.MockOpenAIClient + mockAI *mocks.MockOpenAIService ) var logger *slog.Logger @@ -78,7 +78,7 @@ func TestMain(m *testing.M) { logger.Info("Test server started", "url", testutil.TestServerURL) - mockAI = Handler.OpenAI.(*mocks.MockOpenAIClient) + mockAI = Handler.OpenAI.(*mocks.MockOpenAIService) conversationBuilder = testutil.NewConversationBuilder() code := m.Run() diff --git a/internal/mocks/openai_mock.go b/internal/mocks/openai_mock.go index 93bd72f..de86c28 100644 --- a/internal/mocks/openai_mock.go +++ b/internal/mocks/openai_mock.go @@ -73,21 +73,21 @@ var responseFixtures = map[string]*chatgpt.ChatGPTResponse{ }, } -type MockOpenAIClient struct { +type MockOpenAIService struct { Scenario string } -func NewMockOpenAIClient() *MockOpenAIClient { - mockOpenAIClient := &MockOpenAIClient{} +func NewMockOpenAIService() *MockOpenAIService { + mockOpenAIService := &MockOpenAIService{} - return mockOpenAIClient + return mockOpenAIService } -func (m *MockOpenAIClient) GetChatGPTResponse(prompt string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockOpenAIService) GetChatGPTResponse(prompt string) (*chatgpt.ChatGPTResponse, error) { return responseFixtures[ScenarioInterview], nil } -func (m *MockOpenAIClient) GetChatGPTResponseConversation(_ []map[string]string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockOpenAIService) GetChatGPTResponseConversation(_ []map[string]string) (*chatgpt.ChatGPTResponse, error) { resp, ok := responseFixtures[m.Scenario] if !ok { return nil, fmt.Errorf("invalid scenario: %s", m.Scenario) @@ -95,15 +95,15 @@ func (m *MockOpenAIClient) GetChatGPTResponseConversation(_ []map[string]string) return resp, nil } -func (m *MockOpenAIClient) GetChatGPT35Response(prompt string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockOpenAIService) GetChatGPT35Response(prompt string) (*chatgpt.ChatGPTResponse, error) { return &chatgpt.ChatGPTResponse{}, nil } -func (m *MockOpenAIClient) ExtractJDInput(jd string) (*chatgpt.JDParsedOutput, error) { +func (m *MockOpenAIService) ExtractJDInput(jd string) (*chatgpt.JDParsedOutput, error) { return &chatgpt.JDParsedOutput{}, nil } -func (m *MockOpenAIClient) ExtractJDSummary(jdInput *chatgpt.JDParsedOutput) (string, error) { +func (m *MockOpenAIService) ExtractJDSummary(jdInput *chatgpt.JDParsedOutput) (string, error) { return "", nil } diff --git a/internal/server/server.go b/internal/server/server.go index d14ddff..60aa897 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,8 +35,8 @@ func NewServer(logger *slog.Logger) (*Server, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := chatgpt.NewOpenAI(logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) + openAIService := chatgpt.NewOpenAIService(logger) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAIService, logger) userService := user.NewUserService(userRepo, logger) mailer := mailer.NewMailer(logger) billing, err := billing.NewBilling(billingRepo, userRepo, logger) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index bd230c8..0610ee7 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -37,7 +37,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := mocks.NewMockOpenAIClient() + openAI := mocks.NewMockOpenAIService() mailer := mocks.NewMockMailer() billing, err := billing.NewBilling(billingRepo, userRepo, logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) diff --git a/interview/service_test.go b/interview/service_test.go index c25d537..73d55a9 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -22,7 +22,7 @@ func TestStartInterview(t *testing.T) { length int numQuestions int difficulty string - aiClient *mocks.MockOpenAIClient + aiClient *mocks.MockOpenAIService failRepo bool expected *interview.Interview expectError bool @@ -39,7 +39,7 @@ func TestStartInterview(t *testing.T) { length: 30, numQuestions: 3, difficulty: "easy", - aiClient: &mocks.MockOpenAIClient{}, + aiClient: &mocks.MockOpenAIService{}, expected: &interview.Interview{ UserId: 1, Length: 30, @@ -65,7 +65,7 @@ func TestStartInterview(t *testing.T) { length: 30, numQuestions: 3, difficulty: "easy", - aiClient: &mocks.MockOpenAIClient{}, + aiClient: &mocks.MockOpenAIService{}, failRepo: true, expectError: true, jdSummary: "", @@ -166,7 +166,7 @@ func TestGetInterview(t *testing.T) { repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIClient{}, logger) + interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIService{}, logger) repo.FailRepo = tc.failRepo if tc.setup != nil { diff --git a/learninglog/03_29_2025.md b/learninglog/03_29_2025.md index 8ca7220..d2326c5 100644 --- a/learninglog/03_29_2025.md +++ b/learninglog/03_29_2025.md @@ -10,7 +10,7 @@ just being hardcoded in. As a result, I removed the param and wrote a const vari 2. In needing to write another mock chatGPT response, I'm realizing that I also need to abstract the current interview GetChatGPTResponse method, likely to its own package so that CreateConversations() and AppendConversations() in the conversations package can also access it to both to be able to continue to mock chatGPT responses for my integration tests AND also because the redundancy of the function in both the interview package and the conversation package is just gross and inefficient. ^_^ -However, I currently already have a models package, where the ChatgptResponse struct lives that models the response we get back from OpenAI. I'm thinking I need to get rid of that models package and replace it with a ChatGPT package that could then have its own model and service and interface files. The model file would house the current ChatGPTResponse struct as well as the OpenAIClient struct and AIClient interface currently housed in interview/models. Then I would just import that package and use the resulting method inside the interview package and the two times in the conversation package. +However, I currently already have a models package, where the ChatgptResponse struct lives that models the response we get back from OpenAI. I'm thinking I need to get rid of that models package and replace it with a ChatGPT package that could then have its own model and service and interface files. The model file would house the current ChatGPTResponse struct as well as the OpenAIService struct and AIClient interface currently housed in interview/models. Then I would just import that package and use the resulting method inside the interview package and the two times in the conversation package. ### 🔁 TODO From 839f360bd383d4d7f5fee460717498d44a9ec9f7 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Sun, 12 Oct 2025 16:15:25 +0700 Subject: [PATCH 18/32] further propegation of naming conversion as well as chaning OpenAIService to more scalable naming of AIService --- chatgpt/model.go | 6 +++--- chatgpt/service.go | 10 +++++----- conversation/service_test.go | 4 ++-- handlers/handlers.go | 10 +++++----- handlers/handlers_test.go | 4 ++-- handlers/model.go | 6 +++--- internal/mocks/openai_mock.go | 18 +++++++++--------- internal/server/server.go | 8 ++++---- internal/testutil/server.go | 2 +- interview/service_test.go | 8 ++++---- learninglog/03_29_2025.md | 2 +- mailer/model.go | 6 +++--- mailer/service.go | 8 ++++---- 13 files changed, 46 insertions(+), 46 deletions(-) diff --git a/chatgpt/model.go b/chatgpt/model.go index a7d3ccd..6aec051 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -23,7 +23,7 @@ type ChatGPTResponse struct { Level string `json:"level"` } -type OpenAIService struct { +type AIService struct { APIKey string Logger *slog.Logger } @@ -45,8 +45,8 @@ func (e *OpenAIError) Error() string { return fmt.Sprintf("OpenAI error %d: %s", e.StatusCode, e.Message) } -func NewOpenAIService(logger *slog.Logger) *OpenAIService { - return &OpenAIService{ +func NewAIService(logger *slog.Logger) *AIService { + return &AIService{ APIKey: os.Getenv("OPENAI_API_KEY"), Logger: logger, } diff --git a/chatgpt/service.go b/chatgpt/service.go index b129b26..f388030 100644 --- a/chatgpt/service.go +++ b/chatgpt/service.go @@ -11,7 +11,7 @@ import ( "strings" ) -func (c *OpenAIService) GetChatGPTResponse(prompt string) (*ChatGPTResponse, error) { +func (c *AIService) GetChatGPTResponse(prompt string) (*ChatGPTResponse, error) { ctx := context.Background() var messagesArray []map[string]string @@ -84,7 +84,7 @@ func (c *OpenAIService) GetChatGPTResponse(prompt string) (*ChatGPTResponse, err return &chatGPTResponse, nil } -func (c *OpenAIService) GetChatGPTResponseConversation(conversationHistory []map[string]string) (*ChatGPTResponse, error) { +func (c *AIService) GetChatGPTResponseConversation(conversationHistory []map[string]string) (*ChatGPTResponse, error) { ctx := context.Background() requestBody, err := json.Marshal(map[string]interface{}{ @@ -151,7 +151,7 @@ func (c *OpenAIService) GetChatGPTResponseConversation(conversationHistory []map return &chatGPTResponse, nil } -func (c *OpenAIService) GetChatGPT35Response(prompt string) (*ChatGPTResponse, error) { +func (c *AIService) GetChatGPT35Response(prompt string) (*ChatGPTResponse, error) { ctx := context.Background() var messagesArray []map[string]string @@ -224,7 +224,7 @@ func (c *OpenAIService) GetChatGPT35Response(prompt string) (*ChatGPTResponse, e return &chatGPTResponse, nil } -func (c *OpenAIService) ExtractJDInput(jd string) (*JDParsedOutput, error) { +func (c *AIService) ExtractJDInput(jd string) (*JDParsedOutput, error) { systemPrompt := BuildJDPromptInput(jd) response, err := c.GetChatGPT35Response(systemPrompt) if err != nil { @@ -239,7 +239,7 @@ func (c *OpenAIService) ExtractJDInput(jd string) (*JDParsedOutput, error) { }, nil } -func (c *OpenAIService) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) { +func (c *AIService) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) { jdJSON, err := json.MarshalIndent(jdInput, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal JDParsedOutput: %w", err) diff --git a/conversation/service_test.go b/conversation/service_test.go index 4717753..30f174f 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -15,7 +15,7 @@ import ( ) func TestCreateConversation(t *testing.T) { - ai := &mocks.MockOpenAIService{} + ai := &mocks.MockAIService{} tests := []struct { name string @@ -126,7 +126,7 @@ func TestCreateConversation(t *testing.T) { } func TestAppendConversation(t *testing.T) { - ai := &mocks.MockOpenAIService{} + ai := &mocks.MockAIService{} tests := []struct { name string diff --git a/handlers/handlers.go b/handlers/handlers.go index 381cc92..95fd9a8 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -224,7 +224,7 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - err = h.Billing.CancelSubscription(h.UserRepo, userReturned.Email) + err = h.Billing.CancelSubscription(userReturned.Email) if err != nil { h.Logger.Error("h.Billing.CancelSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -729,7 +729,7 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ conversationCreated, err := conversation.CreateConversation( h.ConversationRepo, h.InterviewRepo, - h.OpenAI, + h.AIService, conversationReturned, interviewID, interviewReturned.Prompt, @@ -813,7 +813,7 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ conversationReturned, err = conversation.AppendConversation( h.ConversationRepo, h.InterviewRepo, - h.OpenAI, + h.AIService, interviewID, userID, conversationReturned, @@ -1371,7 +1371,7 @@ func (h *Handler) JDInputHandler(w http.ResponseWriter, r *http.Request) { return } - jdInput, err := h.OpenAI.ExtractJDInput(input.JobDescription) + jdInput, err := h.AIService.ExtractJDInput(input.JobDescription) if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { @@ -1384,7 +1384,7 @@ func (h *Handler) JDInputHandler(w http.ResponseWriter, r *http.Request) { return } - jdSummary, err := h.OpenAI.ExtractJDSummary(jdInput) + jdSummary, err := h.AIService.ExtractJDSummary(jdInput) if err != nil { var openaiErr *chatgpt.OpenAIError if errors.As(err, &openaiErr) { diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 247a2f1..cd5b832 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -46,7 +46,7 @@ type TestCase struct { var ( Handler *handlers.Handler conversationBuilder *testutil.ConversationBuilder - mockAI *mocks.MockOpenAIService + mockAI *mocks.MockAIService ) var logger *slog.Logger @@ -78,7 +78,7 @@ func TestMain(m *testing.M) { logger.Info("Test server started", "url", testutil.TestServerURL) - mockAI = Handler.OpenAI.(*mocks.MockOpenAIService) + mockAI = Handler.AIService.(*mocks.MockAIService) conversationBuilder = testutil.NewConversationBuilder() code := m.Run() diff --git a/handlers/model.go b/handlers/model.go index 8010e7e..e2d9b5f 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -59,7 +59,7 @@ type Handler struct { TokenService *token.TokenService Billing *billing.Billing Mailer mailer.MailerClient - OpenAI chatgpt.AIClient + AIService chatgpt.AIClient DB *sql.DB Logger *slog.Logger } @@ -71,7 +71,7 @@ func NewHandler( conversationRepo conversation.ConversationRepo, billing *billing.Billing, mailer mailer.MailerClient, - openAI chatgpt.AIClient, + aiService chatgpt.AIClient, db *sql.DB, logger *slog.Logger) *Handler { return &Handler{ @@ -81,7 +81,7 @@ func NewHandler( ConversationRepo: conversationRepo, Billing: billing, Mailer: mailer, - OpenAI: openAI, + AIService: aiService, DB: db, Logger: logger, } diff --git a/internal/mocks/openai_mock.go b/internal/mocks/openai_mock.go index de86c28..7814016 100644 --- a/internal/mocks/openai_mock.go +++ b/internal/mocks/openai_mock.go @@ -73,21 +73,21 @@ var responseFixtures = map[string]*chatgpt.ChatGPTResponse{ }, } -type MockOpenAIService struct { +type MockAIService struct { Scenario string } -func NewMockOpenAIService() *MockOpenAIService { - mockOpenAIService := &MockOpenAIService{} +func NewMockAIService() *MockAIService { + mockAIService := &MockAIService{} - return mockOpenAIService + return mockAIService } -func (m *MockOpenAIService) GetChatGPTResponse(prompt string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockAIService) GetChatGPTResponse(prompt string) (*chatgpt.ChatGPTResponse, error) { return responseFixtures[ScenarioInterview], nil } -func (m *MockOpenAIService) GetChatGPTResponseConversation(_ []map[string]string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockAIService) GetChatGPTResponseConversation(_ []map[string]string) (*chatgpt.ChatGPTResponse, error) { resp, ok := responseFixtures[m.Scenario] if !ok { return nil, fmt.Errorf("invalid scenario: %s", m.Scenario) @@ -95,15 +95,15 @@ func (m *MockOpenAIService) GetChatGPTResponseConversation(_ []map[string]string return resp, nil } -func (m *MockOpenAIService) GetChatGPT35Response(prompt string) (*chatgpt.ChatGPTResponse, error) { +func (m *MockAIService) GetChatGPT35Response(prompt string) (*chatgpt.ChatGPTResponse, error) { return &chatgpt.ChatGPTResponse{}, nil } -func (m *MockOpenAIService) ExtractJDInput(jd string) (*chatgpt.JDParsedOutput, error) { +func (m *MockAIService) ExtractJDInput(jd string) (*chatgpt.JDParsedOutput, error) { return &chatgpt.JDParsedOutput{}, nil } -func (m *MockOpenAIService) ExtractJDSummary(jdInput *chatgpt.JDParsedOutput) (string, error) { +func (m *MockAIService) ExtractJDSummary(jdInput *chatgpt.JDParsedOutput) (string, error) { return "", nil } diff --git a/internal/server/server.go b/internal/server/server.go index 60aa897..bfdd7d9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,10 +35,10 @@ func NewServer(logger *slog.Logger) (*Server, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAIService := chatgpt.NewOpenAIService(logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAIService, logger) + aiService := chatgpt.NewAIService(logger) + interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, aiService, logger) userService := user.NewUserService(userRepo, logger) - mailer := mailer.NewMailer(logger) + mailerService := mailer.NewMailerService(logger) billing, err := billing.NewBilling(billingRepo, userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) if err != nil { @@ -46,7 +46,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billing, mailerService, aiService, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 0610ee7..f9765d6 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -37,7 +37,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := mocks.NewMockOpenAIService() + openAI := mocks.NewMockAIService() mailer := mocks.NewMockMailer() billing, err := billing.NewBilling(billingRepo, userRepo, logger) interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) diff --git a/interview/service_test.go b/interview/service_test.go index 73d55a9..7582530 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -22,7 +22,7 @@ func TestStartInterview(t *testing.T) { length int numQuestions int difficulty string - aiClient *mocks.MockOpenAIService + aiClient *mocks.MockAIService failRepo bool expected *interview.Interview expectError bool @@ -39,7 +39,7 @@ func TestStartInterview(t *testing.T) { length: 30, numQuestions: 3, difficulty: "easy", - aiClient: &mocks.MockOpenAIService{}, + aiClient: &mocks.MockAIService{}, expected: &interview.Interview{ UserId: 1, Length: 30, @@ -65,7 +65,7 @@ func TestStartInterview(t *testing.T) { length: 30, numQuestions: 3, difficulty: "easy", - aiClient: &mocks.MockOpenAIService{}, + aiClient: &mocks.MockAIService{}, failRepo: true, expectError: true, jdSummary: "", @@ -166,7 +166,7 @@ func TestGetInterview(t *testing.T) { repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockOpenAIService{}, logger) + interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockAIService{}, logger) repo.FailRepo = tc.failRepo if tc.setup != nil { diff --git a/learninglog/03_29_2025.md b/learninglog/03_29_2025.md index d2326c5..dad0af9 100644 --- a/learninglog/03_29_2025.md +++ b/learninglog/03_29_2025.md @@ -10,7 +10,7 @@ just being hardcoded in. As a result, I removed the param and wrote a const vari 2. In needing to write another mock chatGPT response, I'm realizing that I also need to abstract the current interview GetChatGPTResponse method, likely to its own package so that CreateConversations() and AppendConversations() in the conversations package can also access it to both to be able to continue to mock chatGPT responses for my integration tests AND also because the redundancy of the function in both the interview package and the conversation package is just gross and inefficient. ^_^ -However, I currently already have a models package, where the ChatgptResponse struct lives that models the response we get back from OpenAI. I'm thinking I need to get rid of that models package and replace it with a ChatGPT package that could then have its own model and service and interface files. The model file would house the current ChatGPTResponse struct as well as the OpenAIService struct and AIClient interface currently housed in interview/models. Then I would just import that package and use the resulting method inside the interview package and the two times in the conversation package. +However, I currently already have a models package, where the ChatgptResponse struct lives that models the response we get back from OpenAI. I'm thinking I need to get rid of that models package and replace it with a ChatGPT package that could then have its own model and service and interface files. The model file would house the current ChatGPTResponse struct as well as the AIService struct and AIClient interface currently housed in interview/models. Then I would just import that package and use the resulting method inside the interview package and the two times in the conversation package. ### 🔁 TODO diff --git a/mailer/model.go b/mailer/model.go index 36170c3..b892f96 100644 --- a/mailer/model.go +++ b/mailer/model.go @@ -5,7 +5,7 @@ import ( "os" ) -type Mailer struct { +type MailerService struct { APIKey string BaseURL string Logger *slog.Logger @@ -23,8 +23,8 @@ const signature = `

` -func NewMailer(logger *slog.Logger) *Mailer { - return &Mailer{ +func NewMailerService(logger *slog.Logger) *MailerService { + return &MailerService{ APIKey: os.Getenv("RESEND_API_KEY"), BaseURL: "https://api.resend.com", Logger: logger, diff --git a/mailer/service.go b/mailer/service.go index 70c3b6e..aa11a10 100644 --- a/mailer/service.go +++ b/mailer/service.go @@ -7,7 +7,7 @@ import ( "net/http" ) -func (m *Mailer) SendPasswordReset(email, resetURL string) error { +func (m *MailerService) SendPasswordReset(email, resetURL string) error { payload := map[string]any{ "from": "Interviewer Support ", "to": email, @@ -48,7 +48,7 @@ func (m *Mailer) SendPasswordReset(email, resetURL string) error { return nil } -func (m *Mailer) SendVerificationEmail(email, verifyURL string) error { +func (m *MailerService) SendVerificationEmail(email, verifyURL string) error { payload := map[string]any{ "from": "Interviewer Support ", @@ -79,7 +79,7 @@ func (m *Mailer) SendVerificationEmail(email, verifyURL string) error { return nil } -func (m *Mailer) SendWelcome(email string) error { +func (m *MailerService) SendWelcome(email string) error { payload := map[string]any{ "from": "Interviewer Support ", "to": email, @@ -128,7 +128,7 @@ func (m *Mailer) SendWelcome(email string) error { return nil } -func (m *Mailer) SendDeletionConfirmation(email string) error { +func (m *MailerService) SendDeletionConfirmation(email string) error { payload := map[string]any{ "from": "Interviewer Support ", "to": email, From 3da4b895c5ba2292438ae823ffadc495cc2d5d1e Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:00:59 +0700 Subject: [PATCH 19/32] further changes to explicit naming of services --- billing/model.go | 6 +++--- billing/service.go | 28 ++++++++++++++-------------- billing/service_test.go | 4 ++-- handlers/model.go | 6 +++--- internal/mocks/mailer_mock.go | 16 +++++++--------- internal/server/server.go | 9 +++++---- internal/testutil/server.go | 11 ++++++----- interview/model.go | 2 +- interview/service_test.go | 4 ++-- 9 files changed, 43 insertions(+), 43 deletions(-) diff --git a/billing/model.go b/billing/model.go index cbd77e9..6a11efc 100644 --- a/billing/model.go +++ b/billing/model.go @@ -11,7 +11,7 @@ import ( "github.com/michaelboegner/interviewer/user" ) -type Billing struct { +type BillingService struct { BillingRepo BillingRepo UserRepo user.UserRepo APIKey string @@ -106,7 +106,7 @@ type BillingRepo interface { MarkWebhookProcessed(id string, event string) error } -func NewBilling(billingRepo BillingRepo, userRepo user.UserRepo, logger *slog.Logger) (*Billing, error) { +func NewBillingService(billingRepo BillingRepo, userRepo user.UserRepo, logger *slog.Logger) (*BillingService, error) { individualID, err := strconv.Atoi(os.Getenv("LEMON_VARIANT_ID_INDIVIDUAL")) if err != nil { return nil, fmt.Errorf("invalid INDIVIDUAL ID: %w", err) @@ -119,7 +119,7 @@ func NewBilling(billingRepo BillingRepo, userRepo user.UserRepo, logger *slog.Lo if err != nil { return nil, fmt.Errorf("invalid PREMIUM ID: %w", err) } - return &Billing{ + return &BillingService{ BillingRepo: billingRepo, UserRepo: userRepo, APIKey: os.Getenv("LEMON_API_KEY"), diff --git a/billing/service.go b/billing/service.go index e314973..6b6312f 100644 --- a/billing/service.go +++ b/billing/service.go @@ -14,7 +14,7 @@ import ( "time" ) -func (b *Billing) RequestCheckoutSession(userEmail string, variantID int) (string, error) { +func (b *BillingService) RequestCheckoutSession(userEmail string, variantID int) (string, error) { payload := CheckoutPayload{ Data: CheckoutData{ Type: "checkouts", @@ -81,7 +81,7 @@ func (b *Billing) RequestCheckoutSession(userEmail string, variantID int) (strin return result.Data.Attributes.URL, nil } -func (b *Billing) RequestDeleteSubscription(subscriptionID string) error { +func (b *BillingService) RequestDeleteSubscription(subscriptionID string) error { client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("DELETE", "https://api.lemonsqueezy.com/v1/subscriptions/"+subscriptionID, nil) @@ -106,7 +106,7 @@ func (b *Billing) RequestDeleteSubscription(subscriptionID string) error { return nil } -func (b *Billing) RequestResumeSubscription(subscriptionID string) error { +func (b *BillingService) RequestResumeSubscription(subscriptionID string) error { client := &http.Client{Timeout: 10 * time.Second} payload := map[string]interface{}{ @@ -146,7 +146,7 @@ func (b *Billing) RequestResumeSubscription(subscriptionID string) error { return nil } -func (b *Billing) RequestUpdateSubscriptionVariant(subscriptionID string, newVariantID int) error { +func (b *BillingService) RequestUpdateSubscriptionVariant(subscriptionID string, newVariantID int) error { payload := map[string]interface{}{ "data": map[string]interface{}{ "type": "subscriptions", @@ -177,14 +177,14 @@ func (b *Billing) RequestUpdateSubscriptionVariant(subscriptionID string, newVar return nil } -func (b *Billing) VerifyBillingSignature(signature string, body []byte, secret string) bool { +func (b *BillingService) VerifyBillingSignature(signature string, body []byte, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } -func (b *Billing) ApplyCredits(email string, variantID int) error { +func (b *BillingService) ApplyCredits(email string, variantID int) error { user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -233,7 +233,7 @@ func (b *Billing) ApplyCredits(email string, variantID int) error { return nil } -func (b *Billing) DeductCredits(orderAttrs OrderAttributes) error { +func (b *BillingService) DeductCredits(orderAttrs OrderAttributes) error { user, err := b.UserRepo.GetUserByEmail(orderAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -284,7 +284,7 @@ func (b *Billing) DeductCredits(orderAttrs OrderAttributes) error { return nil } -func (b *Billing) CreateSubscription(subCreatedAttrs SubscriptionAttributes, subscriptionID string) error { +func (b *BillingService) CreateSubscription(subCreatedAttrs SubscriptionAttributes, subscriptionID string) error { user, err := b.UserRepo.GetUserByEmail(subCreatedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -318,7 +318,7 @@ func (b *Billing) CreateSubscription(subCreatedAttrs SubscriptionAttributes, sub return nil } -func (b *Billing) CancelSubscription(email string) error { +func (b *BillingService) CancelSubscription(email string) error { user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -337,7 +337,7 @@ func (b *Billing) CancelSubscription(email string) error { return nil } -func (b *Billing) ResumeSubscription(email string) error { +func (b *BillingService) ResumeSubscription(email string) error { user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -356,7 +356,7 @@ func (b *Billing) ResumeSubscription(email string) error { return nil } -func (b *Billing) ExpireSubscription(email string) error { +func (b *BillingService) ExpireSubscription(email string) error { user, err := b.UserRepo.GetUserByEmail(email) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -393,7 +393,7 @@ func (b *Billing) ExpireSubscription(email string) error { return nil } -func (b *Billing) RenewSubscription(subRenewAttrs SubscriptionRenewAttributes) error { +func (b *BillingService) RenewSubscription(subRenewAttrs SubscriptionRenewAttributes) error { user, err := b.UserRepo.GetUserByEmail(subRenewAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -439,7 +439,7 @@ func (b *Billing) RenewSubscription(subRenewAttrs SubscriptionRenewAttributes) e return nil } -func (b *Billing) ChangeSubscription(subChangedAttrs SubscriptionAttributes) error { +func (b *BillingService) ChangeSubscription(subChangedAttrs SubscriptionAttributes) error { user, err := b.UserRepo.GetUserByEmail(subChangedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) @@ -488,7 +488,7 @@ func (b *Billing) ChangeSubscription(subChangedAttrs SubscriptionAttributes) err return nil } -func (b *Billing) UpdateSubscription(subUpdatedAttrs SubscriptionAttributes, subscriptionID string) error { +func (b *BillingService) UpdateSubscription(subUpdatedAttrs SubscriptionAttributes, subscriptionID string) error { user, err := b.UserRepo.GetUserByEmail(subUpdatedAttrs.UserEmail) if err != nil { b.Logger.Error("repo.GetUserByEmail failed", "error", err) diff --git a/billing/service_test.go b/billing/service_test.go index 3023319..8c43c3d 100644 --- a/billing/service_test.go +++ b/billing/service_test.go @@ -14,13 +14,13 @@ import ( "github.com/michaelboegner/interviewer/user" ) -func NewTestBilling() *billing.Billing { +func NewTestBilling() *billing.BillingService { handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, }) logger := slog.New(handler) - return &billing.Billing{ + return &billing.BillingService{ VariantIDIndividual: 1, VariantIDPro: 2, VariantIDPremium: 3, diff --git a/handlers/model.go b/handlers/model.go index e2d9b5f..9f58183 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -57,7 +57,7 @@ type Handler struct { InterviewService *interview.InterviewService ConversationRepo conversation.ConversationRepo TokenService *token.TokenService - Billing *billing.Billing + BillingService *billing.BillingService Mailer mailer.MailerClient AIService chatgpt.AIClient DB *sql.DB @@ -69,7 +69,7 @@ func NewHandler( userService *user.UserService, tokenService *token.TokenService, conversationRepo conversation.ConversationRepo, - billing *billing.Billing, + billingService *billing.BillingService, mailer mailer.MailerClient, aiService chatgpt.AIClient, db *sql.DB, @@ -79,7 +79,7 @@ func NewHandler( UserService: userService, TokenService: tokenService, ConversationRepo: conversationRepo, - Billing: billing, + BillingService: billingService, Mailer: mailer, AIService: aiService, DB: db, diff --git a/internal/mocks/mailer_mock.go b/internal/mocks/mailer_mock.go index 05e1c4d..a98d0ef 100644 --- a/internal/mocks/mailer_mock.go +++ b/internal/mocks/mailer_mock.go @@ -1,25 +1,23 @@ package mocks -type MockMailer struct{} +type MockMailerService struct{} -func NewMockMailer() *MockMailer { - mockMailer := &MockMailer{} - - return mockMailer +func NewMockMailerService() *MockMailerService { + return &MockMailerService{} } -func (m *MockMailer) SendPasswordReset(email, resetURL string) error { +func (m *MockMailerService) SendPasswordReset(email, resetURL string) error { return nil } -func (m *MockMailer) SendVerificationEmail(email, verifyURL string) error { +func (m *MockMailerService) SendVerificationEmail(email, verifyURL string) error { return nil } -func (m *MockMailer) SendWelcome(email string) error { +func (m *MockMailerService) SendWelcome(email string) error { return nil } -func (m *MockMailer) SendDeletionConfirmation(email string) error { +func (m *MockMailerService) SendDeletionConfirmation(email string) error { return nil } diff --git a/internal/server/server.go b/internal/server/server.go index bfdd7d9..c712a38 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,18 +35,19 @@ func NewServer(logger *slog.Logger) (*Server, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) + aiService := chatgpt.NewAIService(logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, aiService, logger) + interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, aiService, logger) userService := user.NewUserService(userRepo, logger) - mailerService := mailer.NewMailerService(logger) - billing, err := billing.NewBilling(billingRepo, userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) + mailerService := mailer.NewMailerService(logger) + billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billing, mailerService, aiService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billingService, mailerService, aiService, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index f9765d6..f4d64ae 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -37,18 +37,19 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) - openAI := mocks.NewMockAIService() - mailer := mocks.NewMockMailer() - billing, err := billing.NewBilling(billingRepo, userRepo, logger) - interviewService := interview.NewInterview(interviewRepo, userRepo, billingRepo, openAI, logger) + + mockAIService := mocks.NewMockAIService() + mockMailerService := mocks.NewMockMailerService() + interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, mockAIService, logger) userService := user.NewUserService(userRepo, logger) tokenSerice := token.NewTokenService(tokenRepo, logger) + billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billing, mailer, openAI, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billingService, mockMailerService, mockAIService, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) diff --git a/interview/model.go b/interview/model.go index c5331ca..a82e080 100644 --- a/interview/model.go +++ b/interview/model.go @@ -46,7 +46,7 @@ type InterviewService struct { var ErrNoValidCredits = errors.New("no valid credits") -func NewInterview(interviewRepo InterviewRepo, userRepo user.UserRepo, billingRepo billing.BillingRepo, ai chatgpt.AIClient, logger *slog.Logger) *InterviewService { +func NewInterviewService(interviewRepo InterviewRepo, userRepo user.UserRepo, billingRepo billing.BillingRepo, ai chatgpt.AIClient, logger *slog.Logger) *InterviewService { return &InterviewService{ InterviewRepo: interviewRepo, UserRepo: userRepo, diff --git a/interview/service_test.go b/interview/service_test.go index 7582530..1b51534 100644 --- a/interview/service_test.go +++ b/interview/service_test.go @@ -79,7 +79,7 @@ func TestStartInterview(t *testing.T) { repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, tc.aiClient, logger) + interviewService := interview.NewInterviewService(repo, userRepo, billingRepo, tc.aiClient, logger) repo.FailRepo = tc.failRepo interviewStarted, err := interviewService.StartInterview( @@ -166,7 +166,7 @@ func TestGetInterview(t *testing.T) { repo := interview.NewMockRepo() userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() - interviewService := interview.NewInterview(repo, userRepo, billingRepo, &mocks.MockAIService{}, logger) + interviewService := interview.NewInterviewService(repo, userRepo, billingRepo, &mocks.MockAIService{}, logger) repo.FailRepo = tc.failRepo if tc.setup != nil { From 8480db04bc7516a79f93804e37aa0952c10fcf8a Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:07:11 +0700 Subject: [PATCH 20/32] further clean up for billing services in handlers --- handlers/handlers.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 95fd9a8..ada71dc 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -224,9 +224,9 @@ func (h *Handler) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { return } - err = h.Billing.CancelSubscription(userReturned.Email) + err = h.BillingService.CancelSubscription(userReturned.Email) if err != nil { - h.Logger.Error("h.Billing.CancelSubscription failed", "error", err) + h.Logger.Error("h.BillingService.CancelSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } @@ -290,7 +290,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { jwToken, err := h.TokenService.CreateJWT(strconv.Itoa(userID), 0) if err != nil { log.Printf("JWT creation failed: %v", err) - return "", "", 0, err + RespondWithError(w, http.StatusInternalServerError, "Internal server error") } refreshToken, err := h.TokenService.CreateRefreshToken(userID) @@ -992,7 +992,7 @@ func (h *Handler) CreateCheckoutSessionHandler(w http.ResponseWriter, r *http.Re return } - url, err := h.Billing.RequestCheckoutSession(user.Email, priceIDInt) + url, err := h.BillingService.RequestCheckoutSession(user.Email, priceIDInt) if err != nil { h.Logger.Error("billing.CreateCheckoutSession failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not start checkout") @@ -1021,7 +1021,7 @@ func (h *Handler) CancelSubscriptionHandler(w http.ResponseWriter, r *http.Reque return } - err = h.Billing.RequestDeleteSubscription(userReturned.SubscriptionID) + err = h.BillingService.RequestDeleteSubscription(userReturned.SubscriptionID) if err != nil { h.Logger.Error("DeleteSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not cancel subscription") @@ -1050,7 +1050,7 @@ func (h *Handler) ResumeSubscriptionHandler(w http.ResponseWriter, r *http.Reque return } - err = h.Billing.RequestResumeSubscription(userReturned.SubscriptionID) + err = h.BillingService.RequestResumeSubscription(userReturned.SubscriptionID) if err != nil { h.Logger.Error("DeleteSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Could not cancel subscription") @@ -1103,7 +1103,7 @@ func (h *Handler) ChangePlanHandler(w http.ResponseWriter, r *http.Request) { return } - if err := h.Billing.RequestUpdateSubscriptionVariant(user.SubscriptionID, priceIDInt); err != nil { + if err := h.BillingService.RequestUpdateSubscriptionVariant(user.SubscriptionID, priceIDInt); err != nil { h.Logger.Error("UpdateLemonSubscriptionVariant failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update subscription") return @@ -1127,7 +1127,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) defer r.Body.Close() signature := r.Header.Get("X-Signature") - if !h.Billing.VerifyBillingSignature(signature, body, os.Getenv("LEMON_WEBHOOK_SECRET")) { + if !h.BillingService.VerifyBillingSignature(signature, body, os.Getenv("LEMON_WEBHOOK_SECRET")) { h.Logger.Error("Invalid billing event signature") RespondWithError(w, http.StatusUnauthorized, "Invalid signature") return @@ -1142,9 +1142,9 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) } subscriptionID := webhookPayload.Data.SubscriptionID webhookID := webhookPayload.Meta.WebhookID - exists, err := h.BillingRepo.HasWebhookBeenProcessed(webhookID) + exists, err := h.BillingService.BillingRepo.HasWebhookBeenProcessed(webhookID) if err != nil { - h.Logger.Error("h.BillingRepo.HasWebhookBeenProcessed failed", "error", err) + h.Logger.Error("h.BillingService.BillingRepo.HasWebhookBeenProcessed failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Error checking webhook") return } @@ -1171,9 +1171,9 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.ApplyCredits(h.BillingRepo, orderAttrs.UserEmail, orderAttrs.FirstOrderItem.VariantID) + err = h.BillingService.ApplyCredits(orderAttrs.UserEmail, orderAttrs.FirstOrderItem.VariantID) if err != nil { - h.Logger.Error("h.Billing.ApplyCredits failed", "error", err) + h.Logger.Error("h.BillingService.ApplyCredits failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } @@ -1185,7 +1185,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - exists, err := h.UserRepo.HasActiveOrCancelledSubscription(SubCreatedAttrs.UserEmail) + exists, err := h.UserService.UserRepo.HasActiveOrCancelledSubscription(SubCreatedAttrs.UserEmail) if err != nil { h.Logger.Error("Subscription duplicate check failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Subscription check failed") @@ -1196,7 +1196,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.CreateSubscription(h.UserRepo, SubCreatedAttrs, subscriptionID) + err = h.BillingService.CreateSubscription(SubCreatedAttrs, subscriptionID) if err != nil { h.Logger.Error("h.Billing.CreateSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -1209,9 +1209,9 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.CancelSubscription(h.UserRepo, emailAttribute.UserEmail) + err = h.BillingService.CancelSubscription(emailAttribute.UserEmail) if err != nil { - h.Logger.Error("h.Billing.CancelSubscription failed", "error", err) + h.Logger.Error("h.BillingService.CancelSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } @@ -1222,9 +1222,9 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.ResumeSubscription(h.UserRepo, emailAttribute.UserEmail) + err = h.BillingService.ResumeSubscription(emailAttribute.UserEmail) if err != nil { - h.Logger.Error("h.Billing.ResumeSubscription failed", "error", err) + h.Logger.Error("h.BillingService.ResumeSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") return } @@ -1235,7 +1235,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.ExpireSubscription(h.UserRepo, h.BillingRepo, emailAttribute.UserEmail) + err = h.BillingService.ExpireSubscription(emailAttribute.UserEmail) if err != nil { h.Logger.Error("h.Billing.ExpireSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -1321,7 +1321,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.BillingRepo.MarkWebhookProcessed(webhookID, eventType) + err = h.BillingService.BillingRepo.MarkWebhookProcessed(webhookID, eventType) if err != nil { h.Logger.Error("MarkWebhookProcessed failed", "error", err) w.WriteHeader(http.StatusOK) From 3e05661009ed9561a7905c8778b96d90384efc8b Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:09:34 +0700 Subject: [PATCH 21/32] further cleanup in handlers for billing service naming --- handlers/handlers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index ada71dc..a9f5715 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -1254,7 +1254,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.RenewSubscription(h.UserRepo, h.BillingRepo, SubRenewAttrs) + err = h.BillingService.RenewSubscription(SubRenewAttrs) if err != nil { h.Logger.Error("h.Billing.RenewSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -1268,7 +1268,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.ChangeSubscription(h.UserRepo, h.BillingRepo, SubChangedAttrs) + err = h.BillingService.ChangeSubscription(SubChangedAttrs) if err != nil { h.Logger.Error("h.Billing.ChangeSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -1282,7 +1282,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.UpdateSubscription(h.UserRepo, SubChangedAttrs, subscriptionID) + err = h.BillingService.UpdateSubscription(SubChangedAttrs, subscriptionID) if err != nil { h.Logger.Error("h.Billing.UpdateSubscription failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") @@ -1296,7 +1296,7 @@ func (h *Handler) BillingWebhookHandler(w http.ResponseWriter, r *http.Request) return } - err = h.Billing.DeductCredits(h.UserRepo, h.BillingRepo, orderAttrs) + err = h.BillingService.DeductCredits(orderAttrs) if err != nil { h.Logger.Error("h.Billing.DeductCredits failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Failed to update user") From df12fc9a3b4b64e20c94a0558882e548145794e2 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:17:09 +0700 Subject: [PATCH 22/32] converted dashboard service to struct and methods --- dashboard/model.go | 15 +++++++++++++++ dashboard/service.go | 9 +++------ handlers/handlers.go | 3 +-- handlers/model.go | 4 ++++ internal/server/server.go | 4 +++- internal/testutil/server.go | 4 +++- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/dashboard/model.go b/dashboard/model.go index bc09d9c..2fe36bd 100644 --- a/dashboard/model.go +++ b/dashboard/model.go @@ -1,9 +1,11 @@ package dashboard import ( + "log/slog" "time" "github.com/michaelboegner/interviewer/interview" + "github.com/michaelboegner/interviewer/user" ) type DashboardData struct { @@ -16,3 +18,16 @@ type DashboardData struct { SubscriptionCredits int `json:"subscription_credits"` PastInterviews []interview.Summary `json:"past_interviews"` } + +type DashboardService struct { + UserRepo user.UserRepo + InterviewRepo interview.InterviewRepo + Logger *slog.Logger +} + +func NewDashboardService(userRepo user.UserRepo, interviewRepo interview.InterviewRepo, logger *slog.Logger) *DashboardService { + return &DashboardService{ + UserRepo: userRepo, + InterviewRepo: interviewRepo, + } +} diff --git a/dashboard/service.go b/dashboard/service.go index 773d1e5..03d395f 100644 --- a/dashboard/service.go +++ b/dashboard/service.go @@ -2,19 +2,16 @@ package dashboard import ( "log" - - "github.com/michaelboegner/interviewer/interview" - "github.com/michaelboegner/interviewer/user" ) -func GetDashboardData(userID int, userRepo user.UserRepo, interviewRepo interview.InterviewRepo) (*DashboardData, error) { - user, err := userRepo.GetUser(userID) +func (d *DashboardService) GetDashboardData(userID int) (*DashboardData, error) { + user, err := d.UserRepo.GetUser(userID) if err != nil { log.Printf("GetUser failed for userID %d: %v", userID, err) return nil, err } - interviews, err := interviewRepo.GetInterviewSummariesByUserID(userID) + interviews, err := d.InterviewRepo.GetInterviewSummariesByUserID(userID) if err != nil { log.Printf("GetInterviewSummariesByUserID failed for userID %d: %v", userID, err) return nil, err diff --git a/handlers/handlers.go b/handlers/handlers.go index a9f5715..f8911cf 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -15,7 +15,6 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" - "github.com/michaelboegner/interviewer/dashboard" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/middleware" "github.com/michaelboegner/interviewer/user" @@ -1343,7 +1342,7 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { return } - dashboardData, err := dashboard.GetDashboardData(userID, h.UserRepo, h.InterviewRepo) + dashboardData, err := h.DashboardService.GetDashboardData(userID) if err != nil { if errors.Is(err, sql.ErrNoRows) { RespondWithError(w, http.StatusUnauthorized, "User not found") diff --git a/handlers/model.go b/handlers/model.go index 9f58183..b5a5961 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -7,6 +7,7 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" + "github.com/michaelboegner/interviewer/dashboard" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/mailer" "github.com/michaelboegner/interviewer/token" @@ -60,6 +61,7 @@ type Handler struct { BillingService *billing.BillingService Mailer mailer.MailerClient AIService chatgpt.AIClient + DashboardService *dashboard.DashboardService DB *sql.DB Logger *slog.Logger } @@ -72,6 +74,7 @@ func NewHandler( billingService *billing.BillingService, mailer mailer.MailerClient, aiService chatgpt.AIClient, + dashboardService *dashboard.DashboardService, db *sql.DB, logger *slog.Logger) *Handler { return &Handler{ @@ -82,6 +85,7 @@ func NewHandler( BillingService: billingService, Mailer: mailer, AIService: aiService, + DashboardService: dashboardService, DB: db, Logger: logger, } diff --git a/internal/server/server.go b/internal/server/server.go index c712a38..3369533 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" + "github.com/michaelboegner/interviewer/dashboard" "github.com/michaelboegner/interviewer/database" "github.com/michaelboegner/interviewer/handlers" "github.com/michaelboegner/interviewer/interview" @@ -41,13 +42,14 @@ func NewServer(logger *slog.Logger) (*Server, error) { userService := user.NewUserService(userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) mailerService := mailer.NewMailerService(logger) + dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billingService, mailerService, aiService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billingService, mailerService, aiService, dashboardService, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index f4d64ae..ca8ec47 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -7,6 +7,7 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/conversation" + "github.com/michaelboegner/interviewer/dashboard" "github.com/michaelboegner/interviewer/database" "github.com/michaelboegner/interviewer/handlers" "github.com/michaelboegner/interviewer/internal/mocks" @@ -43,13 +44,14 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, mockAIService, logger) userService := user.NewUserService(userRepo, logger) tokenSerice := token.NewTokenService(tokenRepo, logger) + dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { logger.Error("billing.NewBilling failed", "error", err) return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billingService, mockMailerService, mockAIService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billingService, mockMailerService, mockAIService, dashboardService, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) From 17a3883affdf6ff9c4a52bda51a5aad7559cf7dc Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:36:15 +0700 Subject: [PATCH 23/32] started tackling conversation service conversion to struct and methods --- conversation/model.go | 21 +++++++++++- conversation/service.go | 65 +++++++++++++++++-------------------- handlers/handlers.go | 4 +-- handlers/model.go | 42 ++++++++++++------------ internal/server/server.go | 3 +- internal/testutil/server.go | 3 +- 6 files changed, 76 insertions(+), 62 deletions(-) diff --git a/conversation/model.go b/conversation/model.go index f3fc1a8..e38386b 100644 --- a/conversation/model.go +++ b/conversation/model.go @@ -1,6 +1,11 @@ package conversation -import "time" +import ( + "log/slog" + "time" + + "github.com/michaelboegner/interviewer/interview" +) type Author string @@ -64,6 +69,20 @@ type Message struct { Content string `json:"content"` } +type ConversationService struct { + ConversationRepo ConversationRepo + InterviewRepo interview.InterviewRepo + Logger *slog.Logger +} + +func NewConvesationService(conversationRepo ConversationRepo, interviewRepo interview.InterviewRepo, logger *slog.Logger) *ConversationService { + return &ConversationService{ + ConversationRepo: conversationRepo, + InterviewRepo: interviewRepo, + Logger: logger, + } +} + type ConversationRepo interface { CheckForConversation(interviewID int) (bool, error) GetConversation(interviewID int) (*Conversation, error) diff --git a/conversation/service.go b/conversation/service.go index 4d1d4e1..a530b30 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -5,14 +5,13 @@ import ( "log" "github.com/michaelboegner/interviewer/chatgpt" - "github.com/michaelboegner/interviewer/interview" ) -func CheckForConversation(repo ConversationRepo, interviewID int) (bool, error) { - return repo.CheckForConversation(interviewID) +func (c *ConversationService) CheckForConversation(interviewID int) (bool, error) { + return c.ConversationRepo.CheckForConversation(interviewID) } -func CreateEmptyConversation(repo ConversationRepo, interviewID int, subTopic string) (int, error) { +func (c *ConversationService) CreateEmptyConversation(interviewID int, subTopic string) (int, error) { conversation := &Conversation{ Topics: ClonePredefinedTopics(), CurrentTopic: 1, @@ -20,7 +19,7 @@ func CreateEmptyConversation(repo ConversationRepo, interviewID int, subTopic st CurrentQuestionNumber: 1, } - conversationID, err := repo.CreateConversation(interviewID, conversation) + conversationID, err := c.ConversationRepo.CreateConversation(interviewID, conversation) if err != nil { log.Printf("CreateConversation failed: %v", err) return 0, err @@ -29,9 +28,7 @@ func CreateEmptyConversation(repo ConversationRepo, interviewID int, subTopic st return conversationID, nil } -func CreateConversation( - repo ConversationRepo, - interviewRepo interview.InterviewRepo, +func (c *ConversationService) CreateConversation( openAI chatgpt.AIClient, conversation *Conversation, interviewID int, @@ -44,7 +41,7 @@ func CreateConversation( topicID := conversation.CurrentTopic questionNumber := conversation.CurrentQuestionNumber - _, err := repo.CreateQuestion(conversation, firstQuestion) + _, err := c.ConversationRepo.CreateQuestion(conversation, firstQuestion) if err != nil { log.Printf("CreateQuestion failed: %v", err) return nil, err @@ -60,19 +57,19 @@ func CreateConversation( topic.Questions = make(map[int]*Question) topic.Questions[questionNumber] = NewQuestion(conversationID, topicID, questionNumber, firstQuestion, messages) - err = repo.CreateMessages(conversation, messages) + err = c.ConversationRepo.CreateMessages(conversation, messages) if err != nil { log.Printf("repo.CreateMessages failed: %v", err) return nil, err } - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, c.InterviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err } - err = interviewRepo.UpdateScore(interviewID, chatGPTResponse.Score) + err = c.InterviewRepo.UpdateScore(interviewID, chatGPTResponse.Score) if err != nil { log.Printf("interviewRepo.UpdateScore failed: %v", err) return nil, err @@ -81,7 +78,7 @@ func CreateConversation( conversation.CurrentQuestionNumber++ conversation.CurrentSubtopic = chatGPTResponse.NextSubtopic questionNumber++ - _, err = repo.UpdateConversationCurrents(conversationID, topicID, questionNumber, chatGPTResponse.NextSubtopic) + _, err = c.ConversationRepo.UpdateConversationCurrents(conversationID, topicID, questionNumber, chatGPTResponse.NextSubtopic) if err != nil { log.Printf("UpdateConversationTopic error: %v", err) return nil, err @@ -92,12 +89,12 @@ func CreateConversation( } conversation.Topics[topicID].Questions[questionNumber] = NewQuestion(conversationID, topicID, questionNumber, chatGPTResponse.NextQuestion, messagesQ2) - _, err = repo.AddQuestion(conversation.Topics[topicID].Questions[questionNumber]) + _, err = c.ConversationRepo.AddQuestion(conversation.Topics[topicID].Questions[questionNumber]) if err != nil { log.Printf("AddQuestion in CreateConversation err: %v", err) return nil, err } - _, err = repo.AddMessage(conversationID, topicID, questionNumber, messagesQ2[0]) + _, err = c.ConversationRepo.AddMessage(conversationID, topicID, questionNumber, messagesQ2[0]) if err != nil { log.Printf("AddMessage in CreateConversation err: %v", err) return nil, err @@ -106,9 +103,7 @@ func CreateConversation( return conversation, nil } -func AppendConversation( - repo ConversationRepo, - interviewRepo interview.InterviewRepo, +func (c *ConversationService) AppendConversation( openAI chatgpt.AIClient, interviewID, userID int, @@ -124,19 +119,19 @@ func AppendConversation( } messageUser := NewMessage(conversationID, topicID, questionNumber, User, message) - _, err := repo.AddMessage(conversationID, topicID, questionNumber, messageUser) + _, err := c.ConversationRepo.AddMessage(conversationID, topicID, questionNumber, messageUser) if err != nil { return nil, err } conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageUser) - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, c.InterviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err } - err = interviewRepo.UpdateScore(interviewID, chatGPTResponse.Score) + err = c.InterviewRepo.UpdateScore(interviewID, chatGPTResponse.Score) if err != nil { log.Printf("interviewRepo.UpdateScore failed: %v", err) return nil, err @@ -153,20 +148,20 @@ func AppendConversation( conversation.CurrentSubtopic = "finished" conversation.CurrentQuestionNumber = 0 - err := interviewRepo.UpdateStatus(interviewID, userID, "finished") + err := c.InterviewRepo.UpdateStatus(interviewID, userID, "finished") if err != nil { log.Printf("interviewRepo.UpdateStatus failed: %v", err) return nil, err } - _, err = repo.UpdateConversationCurrents(conversationID, conversation.CurrentTopic, 0, conversation.CurrentSubtopic) + _, err = c.ConversationRepo.UpdateConversationCurrents(conversationID, conversation.CurrentTopic, 0, conversation.CurrentSubtopic) if err != nil { log.Printf("UpdateConversationTopic error: %v", err) return nil, err } messageFinal := NewMessage(conversationID, topicID, questionNumber, Interviewer, chatGPTResponseString) - _, err = repo.AddMessage(conversationID, topicID, questionNumber, messageFinal) + _, err = c.ConversationRepo.AddMessage(conversationID, topicID, questionNumber, messageFinal) if err != nil { return nil, err } @@ -183,7 +178,7 @@ func AppendConversation( conversation.CurrentSubtopic = chatGPTResponse.NextSubtopic conversation.CurrentQuestionNumber = resetQuestionNumber - _, err := repo.UpdateConversationCurrents(conversationID, nextTopicID, resetQuestionNumber, chatGPTResponse.NextSubtopic) + _, err := c.ConversationRepo.UpdateConversationCurrents(conversationID, nextTopicID, resetQuestionNumber, chatGPTResponse.NextSubtopic) if err != nil { log.Printf("UpdateConversationTopic error: %v", err) return nil, err @@ -198,11 +193,11 @@ func AppendConversation( topic.Questions = make(map[int]*Question) topic.Questions[resetQuestionNumber] = question - _, err = repo.AddQuestion(question) + _, err = c.ConversationRepo.AddQuestion(question) if err != nil { log.Printf("AddQuestion in AppendConversation err: %v", err) } - _, err = repo.AddMessage(conversationID, nextTopicID, resetQuestionNumber, messages[0]) + _, err = c.ConversationRepo.AddMessage(conversationID, nextTopicID, resetQuestionNumber, messages[0]) if err != nil { return nil, err } @@ -213,7 +208,7 @@ func AppendConversation( if incrementQuestion { conversation.CurrentQuestionNumber++ questionNumber++ - _, err := repo.UpdateConversationCurrents(conversationID, topicID, questionNumber, chatGPTResponse.NextSubtopic) + _, err := c.ConversationRepo.UpdateConversationCurrents(conversationID, topicID, questionNumber, chatGPTResponse.NextSubtopic) if err != nil { log.Printf("UpdateConversationTopic error: %v", err) return nil, err @@ -225,12 +220,12 @@ func AppendConversation( messageInterviewer := NewMessage(conversationID, topicID, questionNumber, Interviewer, chatGPTResponseString) conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageInterviewer) - _, err = repo.AddQuestion(conversation.Topics[topicID].Questions[questionNumber]) + _, err = c.ConversationRepo.AddQuestion(conversation.Topics[topicID].Questions[questionNumber]) if err != nil { log.Printf("AddQuestion in AppendConversation failed: %v", err) return nil, err } - _, err = repo.AddMessage(conversationID, topicID, questionNumber, messageInterviewer) + _, err = c.ConversationRepo.AddMessage(conversationID, topicID, questionNumber, messageInterviewer) if err != nil { log.Printf("AddMessage in AppendConversation failed: %v", err) return nil, err @@ -239,15 +234,15 @@ func AppendConversation( return conversation, nil } -func GetConversation(repo ConversationRepo, interviewID int) (*Conversation, error) { - conversation, err := repo.GetConversation(interviewID) +func (c *ConversationService) GetConversation(interviewID int) (*Conversation, error) { + conversation, err := c.ConversationRepo.GetConversation(interviewID) if err != nil { return nil, err } conversation.Topics = ClonePredefinedTopics() - questionsReturned, err := repo.GetQuestions(conversation) + questionsReturned, err := c.ConversationRepo.GetQuestions(conversation) if err != nil { return nil, err } @@ -263,9 +258,9 @@ func GetConversation(repo ConversationRepo, interviewID int) (*Conversation, err topic.Questions[question.QuestionNumber] = question - messagesReturned, err := repo.GetMessages(conversation.ID, topicID, question.QuestionNumber) + messagesReturned, err := c.ConversationRepo.GetMessages(conversation.ID, topicID, question.QuestionNumber) if err != nil { - log.Printf("repo.GetMessages failed: %v\n", err) + log.Printf("c.ConversationRepo.GetMessages failed: %v\n", err) return nil, err } diff --git a/handlers/handlers.go b/handlers/handlers.go index f8911cf..54d4606 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -725,9 +725,7 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ return } - conversationCreated, err := conversation.CreateConversation( - h.ConversationRepo, - h.InterviewRepo, + conversationCreated, err := h.ConversationService.CreateConversation( h.AIService, conversationReturned, interviewID, diff --git a/handlers/model.go b/handlers/model.go index b5a5961..3b77ac9 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -54,23 +54,23 @@ type ReturnVals struct { } type Handler struct { - UserService *user.UserService - InterviewService *interview.InterviewService - ConversationRepo conversation.ConversationRepo - TokenService *token.TokenService - BillingService *billing.BillingService - Mailer mailer.MailerClient - AIService chatgpt.AIClient - DashboardService *dashboard.DashboardService - DB *sql.DB - Logger *slog.Logger + UserService *user.UserService + InterviewService *interview.InterviewService + ConversationService *conversation.ConversationService + TokenService *token.TokenService + BillingService *billing.BillingService + Mailer mailer.MailerClient + AIService chatgpt.AIClient + DashboardService *dashboard.DashboardService + DB *sql.DB + Logger *slog.Logger } func NewHandler( interviewService *interview.InterviewService, userService *user.UserService, tokenService *token.TokenService, - conversationRepo conversation.ConversationRepo, + conversationService *conversation.ConversationService, billingService *billing.BillingService, mailer mailer.MailerClient, aiService chatgpt.AIClient, @@ -78,15 +78,15 @@ func NewHandler( db *sql.DB, logger *slog.Logger) *Handler { return &Handler{ - InterviewService: interviewService, - UserService: userService, - TokenService: tokenService, - ConversationRepo: conversationRepo, - BillingService: billingService, - Mailer: mailer, - AIService: aiService, - DashboardService: dashboardService, - DB: db, - Logger: logger, + InterviewService: interviewService, + UserService: userService, + TokenService: tokenService, + ConversationService: conversationService, + BillingService: billingService, + Mailer: mailer, + AIService: aiService, + DashboardService: dashboardService, + DB: db, + Logger: logger, } } diff --git a/internal/server/server.go b/internal/server/server.go index 3369533..18d9d15 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -41,6 +41,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, aiService, logger) userService := user.NewUserService(userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) mailerService := mailer.NewMailerService(logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) @@ -49,7 +50,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenService, conversationRepo, billingService, mailerService, aiService, dashboardService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationService, billingService, mailerService, aiService, dashboardService, db, logger) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index ca8ec47..2db6c86 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -44,6 +44,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, mockAIService, logger) userService := user.NewUserService(userRepo, logger) tokenSerice := token.NewTokenService(tokenRepo, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { @@ -51,7 +52,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationRepo, billingService, mockMailerService, mockAIService, dashboardService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationService, billingService, mockMailerService, mockAIService, dashboardService, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) From fd90ac4cea3ec964d90c6d49bf745ec4ccf9d941 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:38:31 +0700 Subject: [PATCH 24/32] cleaned up conversation naming in handlers --- handlers/handlers.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/handlers/handlers.go b/handlers/handlers.go index 54d4606..99c8894 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -14,7 +14,6 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" - "github.com/michaelboegner/interviewer/conversation" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/middleware" "github.com/michaelboegner/interviewer/user" @@ -563,7 +562,7 @@ func (h *Handler) InterviewsHandler(w http.ResponseWriter, r *http.Request) { return } - conversationID, err := conversation.CreateEmptyConversation(h.ConversationRepo, interviewStarted.Id, interviewStarted.Subtopic) + conversationID, err := h.ConversationService.CreateEmptyConversation(interviewStarted.Id, interviewStarted.Subtopic) if err != nil { h.Logger.Error("conversation.CreateEmptyConversation failed", "error", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") @@ -718,7 +717,7 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ return } - conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) + conversationReturned, err := h.ConversationService.GetConversation(interviewID) if err != nil { h.Logger.Error("conversation.GetConversation failed", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID") @@ -800,16 +799,14 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ return } - conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) + conversationReturned, err := h.ConversationService.GetConversation(interviewID) if err != nil { h.Logger.Error("GetConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID.") return } - conversationReturned, err = conversation.AppendConversation( - h.ConversationRepo, - h.InterviewRepo, + conversationReturned, err = h.ConversationService.AppendConversation( h.AIService, interviewID, userID, @@ -865,7 +862,7 @@ func (h *Handler) GetConversationHandler(w http.ResponseWriter, r *http.Request) return } - conversationReturned, err := conversation.GetConversation(h.ConversationRepo, interviewID) + conversationReturned, err := h.ConversationService.GetConversation(interviewID) if err != nil { h.Logger.Error("GetConversation error", "error", err) RespondWithError(w, http.StatusBadRequest, "Invalid ID.") From 3d10acc19e6948ad756522a95916e31d45c4f081 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 13 Oct 2025 16:46:43 +0700 Subject: [PATCH 25/32] cleaned up tests --- conversation/service_test.go | 41 ++++++++++++++++-------------------- handlers/handlers_test.go | 4 ++-- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/conversation/service_test.go b/conversation/service_test.go index 30f174f..31dcf58 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -3,6 +3,7 @@ package conversation_test import ( "fmt" "log" + "log/slog" "os" "strings" "testing" @@ -84,19 +85,13 @@ func TestCreateConversation(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - if tc.setup != nil { - tc.setup() - } - - repo := conversation.NewMockRepo() + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - repo.FailRepo = tc.failRepo + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + conversationRepo.FailRepo = tc.failRepo - convo, err := conversation.CreateConversation( - repo, - interviewRepo, + convo, err := conversationService.CreateConversation( ai, tc.convo, tc.interviewID, @@ -181,19 +176,13 @@ func TestAppendConversation(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) - if tc.setup != nil { - tc.setup() - } - - repo := conversation.NewMockRepo() + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) + conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - repo.FailRepo = tc.failRepo + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + conversationRepo.FailRepo = tc.failRepo - convo, err := conversation.CreateConversation( - repo, - interviewRepo, + convo, err := conversationService.CreateConversation( ai, tc.convo, tc.interviewID, @@ -209,7 +198,13 @@ func TestAppendConversation(t *testing.T) { t.Fatalf("failed to create initial conversation: %v", err) } - updatedConvo, err := conversation.AppendConversation(repo, interviewRepo, ai, tc.interviewID, tc.userID, convo, tc.message, tc.prompt) + updatedConvo, err := conversationService.AppendConversation( + ai, + tc.interviewID, + tc.userID, + convo, + tc.message, + tc.prompt) if tc.expectError && err == nil { t.Fatalf("expected error but got nil") diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index cd5b832..9b5fdbc 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -859,7 +859,7 @@ func Test_CreateConversationsHandler_Integration(t *testing.T) { // Assert Database if tc.DBCheck { - conversation, err := conversation.GetConversation(Handler.ConversationRepo, got.Conversation.ID) + conversation, err := Handler.ConversationService.GetConversation(got.Conversation.ID) if err != nil { t.Fatalf("Assert Database: GetConversation failed: %v", err) } @@ -999,7 +999,7 @@ func Test_AppendConversationsHandler_Integration(t *testing.T) { // DB validation if tc.DBCheck { - gotDB, err := conversation.GetConversation(Handler.ConversationRepo, respUnmarshalled.Conversation.ID) + gotDB, err := Handler.ConversationService.GetConversation(respUnmarshalled.Conversation.ID) if err != nil { t.Fatalf("DB check failed: %v", err) } From 70e01ef702f785102d99617f029533d0b9f988e5 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 14 Oct 2025 15:13:37 +0700 Subject: [PATCH 26/32] conversation converted to service struct and methods --- conversation/model.go | 5 ++++- conversation/service.go | 8 ++------ conversation/service_test.go | 10 +++++----- handlers/handlers.go | 2 -- internal/server/server.go | 2 +- internal/testutil/server.go | 2 +- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/conversation/model.go b/conversation/model.go index e38386b..b2ecf7e 100644 --- a/conversation/model.go +++ b/conversation/model.go @@ -4,6 +4,7 @@ import ( "log/slog" "time" + "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/interview" ) @@ -72,13 +73,15 @@ type Message struct { type ConversationService struct { ConversationRepo ConversationRepo InterviewRepo interview.InterviewRepo + AIService chatgpt.AIClient Logger *slog.Logger } -func NewConvesationService(conversationRepo ConversationRepo, interviewRepo interview.InterviewRepo, logger *slog.Logger) *ConversationService { +func NewConvesationService(conversationRepo ConversationRepo, interviewRepo interview.InterviewRepo, aiService chatgpt.AIClient, logger *slog.Logger) *ConversationService { return &ConversationService{ ConversationRepo: conversationRepo, InterviewRepo: interviewRepo, + AIService: aiService, Logger: logger, } } diff --git a/conversation/service.go b/conversation/service.go index a530b30..1081b88 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -3,8 +3,6 @@ package conversation import ( "errors" "log" - - "github.com/michaelboegner/interviewer/chatgpt" ) func (c *ConversationService) CheckForConversation(interviewID int) (bool, error) { @@ -29,7 +27,6 @@ func (c *ConversationService) CreateEmptyConversation(interviewID int, subTopic } func (c *ConversationService) CreateConversation( - openAI chatgpt.AIClient, conversation *Conversation, interviewID int, prompt, @@ -63,7 +60,7 @@ func (c *ConversationService) CreateConversation( return nil, err } - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, c.InterviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, c.AIService, c.InterviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err @@ -104,7 +101,6 @@ func (c *ConversationService) CreateConversation( } func (c *ConversationService) AppendConversation( - openAI chatgpt.AIClient, interviewID, userID int, conversation *Conversation, @@ -125,7 +121,7 @@ func (c *ConversationService) AppendConversation( } conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageUser) - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, c.InterviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, c.AIService, c.InterviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err diff --git a/conversation/service_test.go b/conversation/service_test.go index 31dcf58..74ecec2 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" "github.com/michaelboegner/interviewer/internal/mocks" "github.com/michaelboegner/interviewer/interview" @@ -88,11 +89,11 @@ func TestCreateConversation(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + aiService := chatgpt.NewAIService(logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( - ai, tc.convo, tc.interviewID, tc.prompt, @@ -179,11 +180,11 @@ func TestAppendConversation(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + aiService := chatgpt.NewAIService(logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( - ai, tc.convo, tc.interviewID, "Prompt", @@ -199,7 +200,6 @@ func TestAppendConversation(t *testing.T) { } updatedConvo, err := conversationService.AppendConversation( - ai, tc.interviewID, tc.userID, convo, diff --git a/handlers/handlers.go b/handlers/handlers.go index 99c8894..7fbe74b 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -725,7 +725,6 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ } conversationCreated, err := h.ConversationService.CreateConversation( - h.AIService, conversationReturned, interviewID, interviewReturned.Prompt, @@ -807,7 +806,6 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ } conversationReturned, err = h.ConversationService.AppendConversation( - h.AIService, interviewID, userID, conversationReturned, diff --git a/internal/server/server.go b/internal/server/server.go index 18d9d15..b5ab8c1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -41,7 +41,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, aiService, logger) userService := user.NewUserService(userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) mailerService := mailer.NewMailerService(logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 2db6c86..7926a88 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -44,7 +44,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, mockAIService, logger) userService := user.NewUserService(userRepo, logger) tokenSerice := token.NewTokenService(tokenRepo, logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { From 997414b83bec1447af8218fd7dad605b5e392623 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 14 Oct 2025 15:39:33 +0700 Subject: [PATCH 27/32] fixed conversation tests --- conversation/service_test.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/conversation/service_test.go b/conversation/service_test.go index 74ecec2..5011ec3 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -1,23 +1,19 @@ package conversation_test import ( - "fmt" - "log" "log/slog" - "os" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" "github.com/michaelboegner/interviewer/internal/mocks" "github.com/michaelboegner/interviewer/interview" ) func TestCreateConversation(t *testing.T) { - ai := &mocks.MockAIService{} + mockAIService := &mocks.MockAIService{} tests := []struct { name string @@ -59,7 +55,7 @@ func TestCreateConversation(t *testing.T) { CurrentQuestionNumber: 3, }, setup: func() { - ai.Scenario = mocks.ScenarioCreated + mockAIService.Scenario = mocks.ScenarioCreated }, }, { @@ -85,12 +81,14 @@ func TestCreateConversation(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + if tc.setup != nil { + tc.setup() + } var buf strings.Builder logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - aiService := chatgpt.NewAIService(logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( @@ -122,8 +120,7 @@ func TestCreateConversation(t *testing.T) { } func TestAppendConversation(t *testing.T) { - ai := &mocks.MockAIService{} - + mockAIService := &mocks.MockAIService{} tests := []struct { name string message string @@ -152,7 +149,7 @@ func TestAppendConversation(t *testing.T) { failRepo: false, expectError: false, setup: func() { - ai.Scenario = mocks.ScenarioAppended1 + mockAIService.Scenario = mocks.ScenarioAppended1 }, }, { @@ -176,12 +173,14 @@ func TestAppendConversation(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + if tc.setup != nil { + tc.setup() + } var buf strings.Builder logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - aiService := chatgpt.NewAIService(logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) + conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( @@ -220,10 +219,3 @@ func TestAppendConversation(t *testing.T) { }) } } - -func showLogsIfFail(t *testing.T, name string, buf strings.Builder) { - log.SetOutput(os.Stderr) - if t.Failed() { - fmt.Printf("---- logs for test: %s ----\n%s\n", name, buf.String()) - } -} From 9b71689e0562efd689ff2ec3d9ed7c2d285e8287 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 14 Oct 2025 16:19:56 +0700 Subject: [PATCH 28/32] fixed billing service tests --- billing/service_test.go | 74 ++++++++++++++++++++++------------------- user/repository_mock.go | 27 +++++++-------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/billing/service_test.go b/billing/service_test.go index 8c43c3d..9cb65c4 100644 --- a/billing/service_test.go +++ b/billing/service_test.go @@ -4,23 +4,19 @@ import ( "crypto/hmac" "crypto/sha256" "fmt" - "log" "log/slog" "os" - "strings" "testing" "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/user" ) -func NewTestBilling() *billing.BillingService { - handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - Level: slog.LevelDebug, - }) - logger := slog.New(handler) - +func NewTestBillingService(billingRepo billing.BillingRepo, userRepo user.UserRepo, logger *slog.Logger) *billing.BillingService { return &billing.BillingService{ + BillingRepo: billingRepo, + UserRepo: userRepo, + APIKey: "", VariantIDIndividual: 1, VariantIDPro: 2, VariantIDPremium: 3, @@ -89,20 +85,24 @@ func TestApplyCredits(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) - userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() + userRepo := user.NewMockRepo() - userRepo.FailGetUserByEmail = tc.failUser - userRepo.FailAddCredits = tc.failCredit - billingRepo.FailLogCreditTransaction = tc.failLog + billingService := NewTestBillingService(billingRepo, userRepo, logger) - b := NewTestBilling() + if mockUserRepo, ok := billingService.UserRepo.(*user.MockRepo); ok { + mockUserRepo.FailGetUserByEmail = tc.failUser + mockUserRepo.FailAddCredits = tc.failCredit + } + if mockBillingRepo, ok := billingService.BillingRepo.(*billing.MockRepo); ok { + mockBillingRepo.FailLogCreditTransaction = tc.failLog + } + + err := billingService.ApplyCredits("test@example.com", tc.variantID) - err := b.ApplyCredits("test@example.com", tc.variantID) if tc.expectErr && err == nil { t.Fatal("expected error but got nil") } @@ -166,18 +166,21 @@ func TestDeductCredits(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var buf strings.Builder - log.SetOutput(&buf) - defer showLogsIfFail(t, tc.name, buf) + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) - userRepo := user.NewMockRepo() billingRepo := billing.NewMockRepo() + userRepo := user.NewMockRepo() - userRepo.FailGetUserByEmail = tc.failUser - userRepo.FailAddCredits = tc.failCredit - billingRepo.FailLogCreditTransaction = tc.failLog + billingService := NewTestBillingService(billingRepo, userRepo, logger) - b := NewTestBilling() + if mockUserRepo, ok := billingService.UserRepo.(*user.MockRepo); ok { + mockUserRepo.FailGetUserByEmail = tc.failUser + mockUserRepo.FailAddCredits = tc.failCredit + } + if mockBillingRepo, ok := billingService.BillingRepo.(*billing.MockRepo); ok { + mockBillingRepo.FailLogCreditTransaction = tc.failLog + } attrs := billing.OrderAttributes{ UserEmail: "test@example.com", @@ -188,7 +191,7 @@ func TestDeductCredits(t *testing.T) { }, } - err := b.DeductCredits(attrs) + err := billingService.DeductCredits(attrs) if tc.expectErr && err == nil { t.Fatal("expected error but got nil") } @@ -200,14 +203,21 @@ func TestDeductCredits(t *testing.T) { } func TestVerifyBillingSignature(t *testing.T) { - b := NewTestBilling() + handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) + + billingRepo := billing.NewMockRepo() + userRepo := user.NewMockRepo() + + billingService := NewTestBillingService(billingRepo, userRepo, logger) + body := []byte(`{"key":"value"}`) secret := "testsecret" mac := hmacSha256(body, secret) - if !b.VerifyBillingSignature(mac, body, secret) { + if !billingService.VerifyBillingSignature(mac, body, secret) { t.Fatal("expected signature to be valid") } - if b.VerifyBillingSignature("invalid", body, secret) { + if billingService.VerifyBillingSignature("invalid", body, secret) { t.Fatal("expected signature to be invalid") } } @@ -217,9 +227,3 @@ func hmacSha256(message []byte, secret string) string { mac.Write(message) return fmt.Sprintf("%x", mac.Sum(nil)) } - -func showLogsIfFail(t *testing.T, name string, buf strings.Builder) { - if t.Failed() { - t.Logf("---- logs for test: %s ----\n%s\n", name, buf.String()) - } -} diff --git a/user/repository_mock.go b/user/repository_mock.go index 6dc182a..19fd6ff 100644 --- a/user/repository_mock.go +++ b/user/repository_mock.go @@ -10,7 +10,7 @@ import ( type MockRepo struct { Users map[int]User - failRepo bool + FailRepo bool FailGetUserByEmail bool FailAddCredits bool } @@ -27,12 +27,13 @@ func NewMockRepo() *MockRepo { } return &MockRepo{ - Users: map[int]User{}, + Users: map[int]User{}, + FailGetUserByEmail: false, } } func (m *MockRepo) CreateUser(user *User) (int, error) { - if m.failRepo { + if m.FailRepo { return 0, errors.New("Mocked DB failure") } @@ -40,7 +41,7 @@ func (m *MockRepo) CreateUser(user *User) (int, error) { } func (m *MockRepo) MarkUserDeleted(userID int) error { - if m.failRepo { + if m.FailRepo { return errors.New("Mocked DB failure") } @@ -48,7 +49,7 @@ func (m *MockRepo) MarkUserDeleted(userID int) error { } func (m *MockRepo) GetUser(userID int) (*User, error) { - if m.failRepo { + if m.FailRepo { return nil, errors.New("Mocked DB failure") } @@ -64,7 +65,7 @@ func (m *MockRepo) GetUser(userID int) (*User, error) { } func (m *MockRepo) GetPasswordandID(username string) (int, string, error) { - if m.failRepo { + if m.FailRepo { return 0, "", errors.New("Mocked DB failure") } @@ -75,7 +76,7 @@ func (m *MockRepo) GetUserByEmail(email string) (*User, error) { if m.FailGetUserByEmail { return nil, errors.New("Mocked GetUserByEmail failure") } - if m.failRepo { + if m.FailRepo { return nil, errors.New("Mocked DB failure") } @@ -90,7 +91,7 @@ func (m *MockRepo) GetUserByEmail(email string) (*User, error) { } func (m *MockRepo) GetUserByCustomerID(customerID string) (*User, error) { - if m.failRepo { + if m.FailRepo { return nil, errors.New("Mocked DB failure") } @@ -105,7 +106,7 @@ func (m *MockRepo) GetUserByCustomerID(customerID string) (*User, error) { } func (m *MockRepo) UpdatePasswordByEmail(email string, password []byte) error { - if m.failRepo { + if m.FailRepo { return errors.New("Mocked DB failure") } @@ -116,7 +117,7 @@ func (m *MockRepo) AddCredits(userID, credits int, creditType string) error { if m.FailAddCredits { return errors.New("Mocked AddCredits failure") } - if m.failRepo { + if m.FailRepo { return errors.New("Mocked DB failure") } @@ -124,7 +125,7 @@ func (m *MockRepo) AddCredits(userID, credits int, creditType string) error { } func (m *MockRepo) UpdateSubscriptionData(userID int, status, tier, subscriptionID string, startsAt, endsAt time.Time) error { - if m.failRepo { + if m.FailRepo { return errors.New("Mocked DB failure") } @@ -132,7 +133,7 @@ func (m *MockRepo) UpdateSubscriptionData(userID int, status, tier, subscription } func (m *MockRepo) UpdateSubscriptionStatusData(userID int, status string) error { - if m.failRepo { + if m.FailRepo { return errors.New("Mocked DB failure") } @@ -140,7 +141,7 @@ func (m *MockRepo) UpdateSubscriptionStatusData(userID int, status string) error } func (m *MockRepo) HasActiveOrCancelledSubscription(email string) (bool, error) { - if m.failRepo { + if m.FailRepo { return false, errors.New("Mocked DB failure") } From 78f8476989138ff34557ca08df39196fa0d11280 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Tue, 14 Oct 2025 16:27:15 +0700 Subject: [PATCH 29/32] fixing user service test --- user/service_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user/service_test.go b/user/service_test.go index 146181e..7c4abe9 100644 --- a/user/service_test.go +++ b/user/service_test.go @@ -55,7 +55,7 @@ func TestCreateUser(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) userRepo := NewMockRepo() userService := NewUserService(userRepo, logger) - userRepo.failRepo = tc.failRepo + userRepo.FailRepo = tc.failRepo jwt, err := userService.VerificationToken(tc.email, tc.username, tc.password) if err != nil { @@ -125,7 +125,7 @@ func TestLoginUser(t *testing.T) { tokenRepo := token.NewMockRepo() userService := NewUserService(userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) - userRepo.failRepo = tc.failRepo + userRepo.FailRepo = tc.failRepo username, userID, err := userService.LoginUser(tc.email, tc.password) if err != nil { @@ -195,7 +195,7 @@ func TestGetUser(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) userRepo := NewMockRepo() userService := NewUserService(userRepo, logger) - userRepo.failRepo = tc.failRepo + userRepo.FailRepo = tc.failRepo user, err := userService.GetUser(tc.userID) @@ -247,7 +247,7 @@ func TestUpdateSubscription(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) userRepo := NewMockRepo() userService := NewUserService(userRepo, logger) - userRepo.failRepo = tc.failRepo + userRepo.FailRepo = tc.failRepo user, err := userService.GetUser(tc.userID) From c4a0eb0c39380d3c3243d4f6ce1ae8a3f01a04a8 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Thu, 16 Oct 2025 13:19:44 +0700 Subject: [PATCH 30/32] fixed TestLoginUser unit test --- user/service_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/user/service_test.go b/user/service_test.go index 7c4abe9..169f12f 100644 --- a/user/service_test.go +++ b/user/service_test.go @@ -128,8 +128,15 @@ func TestLoginUser(t *testing.T) { userRepo.FailRepo = tc.failRepo username, userID, err := userService.LoginUser(tc.email, tc.password) + if tc.expectError { + if err == nil { + t.Fatalf("expected error but got nil") + } + return + } + if err != nil { - t.Fatalf("userService.LoginUser failed: %v", err) + t.Fatalf("did not expect error but got: %v", err) } jwToken, err = tokenService.CreateJWT(strconv.Itoa(userID), 0) @@ -137,13 +144,6 @@ func TestLoginUser(t *testing.T) { t.Fatalf("JWT creation failed: %v", err) } - if tc.expectError && err == nil { - t.Fatalf("expected error but got nil") - } - if !tc.expectError && err != nil { - t.Fatalf("did not expect error but got: %v", err) - } - if !tc.expectError { expected := tc.userID got := userID From dbd06fc07ef9e5bd051c41e98d5a8f53c353bbe5 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Fri, 17 Oct 2025 14:20:47 +0700 Subject: [PATCH 31/32] fixing copilot suggestions --- conversation/model.go | 2 +- conversation/service_test.go | 4 ++-- dashboard/model.go | 1 + handlers/handlers.go | 1 + internal/server/server.go | 2 +- internal/testutil/server.go | 6 +++--- interview/service.go | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/conversation/model.go b/conversation/model.go index b2ecf7e..052654f 100644 --- a/conversation/model.go +++ b/conversation/model.go @@ -77,7 +77,7 @@ type ConversationService struct { Logger *slog.Logger } -func NewConvesationService(conversationRepo ConversationRepo, interviewRepo interview.InterviewRepo, aiService chatgpt.AIClient, logger *slog.Logger) *ConversationService { +func NewConversationService(conversationRepo ConversationRepo, interviewRepo interview.InterviewRepo, aiService chatgpt.AIClient, logger *slog.Logger) *ConversationService { return &ConversationService{ ConversationRepo: conversationRepo, InterviewRepo: interviewRepo, diff --git a/conversation/service_test.go b/conversation/service_test.go index 5011ec3..03a503b 100644 --- a/conversation/service_test.go +++ b/conversation/service_test.go @@ -88,7 +88,7 @@ func TestCreateConversation(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) + conversationService := conversation.NewConversationService(conversationRepo, interviewRepo, mockAIService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( @@ -180,7 +180,7 @@ func TestAppendConversation(t *testing.T) { logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})) conversationRepo := conversation.NewMockRepo() interviewRepo := interview.NewMockRepo() - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) + conversationService := conversation.NewConversationService(conversationRepo, interviewRepo, mockAIService, logger) conversationRepo.FailRepo = tc.failRepo convo, err := conversationService.CreateConversation( diff --git a/dashboard/model.go b/dashboard/model.go index 2fe36bd..6151e6d 100644 --- a/dashboard/model.go +++ b/dashboard/model.go @@ -29,5 +29,6 @@ func NewDashboardService(userRepo user.UserRepo, interviewRepo interview.Intervi return &DashboardService{ UserRepo: userRepo, InterviewRepo: interviewRepo, + Logger: logger, } } diff --git a/handlers/handlers.go b/handlers/handlers.go index 7ddc3d8..9a5dd6c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -270,6 +270,7 @@ func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("JWT creation failed: %v", err) RespondWithError(w, http.StatusInternalServerError, "Internal server error") + return } refreshToken, err := h.TokenService.CreateRefreshToken(userID) diff --git a/internal/server/server.go b/internal/server/server.go index b5ab8c1..058e594 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -41,7 +41,7 @@ func NewServer(logger *slog.Logger) (*Server, error) { interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, aiService, logger) userService := user.NewUserService(userRepo, logger) tokenService := token.NewTokenService(tokenRepo, logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, aiService, logger) + conversationService := conversation.NewConversationService(conversationRepo, interviewRepo, aiService, logger) mailerService := mailer.NewMailerService(logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 7926a88..011f5ac 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -43,8 +43,8 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { mockMailerService := mocks.NewMockMailerService() interviewService := interview.NewInterviewService(interviewRepo, userRepo, billingRepo, mockAIService, logger) userService := user.NewUserService(userRepo, logger) - tokenSerice := token.NewTokenService(tokenRepo, logger) - conversationService := conversation.NewConvesationService(conversationRepo, interviewRepo, mockAIService, logger) + tokenService := token.NewTokenService(tokenRepo, logger) + conversationService := conversation.NewConversationService(conversationRepo, interviewRepo, mockAIService, logger) dashboardService := dashboard.NewDashboardService(userRepo, interviewRepo, logger) billingService, err := billing.NewBillingService(billingRepo, userRepo, logger) if err != nil { @@ -52,7 +52,7 @@ func InitTestServer(logger *slog.Logger) (*handlers.Handler, error) { return nil, err } - handler := handlers.NewHandler(interviewService, userService, tokenSerice, conversationService, billingService, mockMailerService, mockAIService, dashboardService, db, logger) + handler := handlers.NewHandler(interviewService, userService, tokenService, conversationService, billingService, mockMailerService, mockAIService, dashboardService, db, logger) TestMux = http.NewServeMux() TestMux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) diff --git a/interview/service.go b/interview/service.go index b1a4e33..410ba7c 100644 --- a/interview/service.go +++ b/interview/service.go @@ -101,7 +101,7 @@ func canUseCredit(user *user.User, logger *slog.Logger) (string, error) { user.SubscriptionEndDate.After(now) && user.SubscriptionStatus != "expired" && user.SubscriptionCredits > 0: - logger.Info("subscrtipion plan in canUseCredit check") + logger.Info("subscription plan in canUseCredit check") return "subscription", nil case user.IndividualCredits > 0: logger.Info("individual plan in canUseCredit check") From bc2aa2903a9f1368aa581d92277afe8f020b6903 Mon Sep 17 00:00:00 2001 From: MichaelBoegner Date: Mon, 20 Oct 2025 11:40:56 +0700 Subject: [PATCH 32/32] converted deductAndLogCredit and canUseCredit in interview service to methods to maintain consistency --- interview/service.go | 57 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/interview/service.go b/interview/service.go index 410ba7c..f0ffda6 100644 --- a/interview/service.go +++ b/interview/service.go @@ -2,7 +2,6 @@ package interview import ( "fmt" - "log/slog" "time" "github.com/michaelboegner/interviewer/billing" @@ -17,7 +16,7 @@ func (i *InterviewService) StartInterview( difficulty string, jd string) (*Interview, error) { - err := deductAndLogCredit(user, i.UserRepo, i.BillingRepo, i.Logger) + err := i.deductAndLogCredit(user) if err != nil { i.Logger.Error("checkCreditsLogTransaction failed", "error", err) return nil, err @@ -93,39 +92,20 @@ func (i *InterviewService) GetInterview(interviewID int) (*Interview, error) { return interview, nil } -func canUseCredit(user *user.User, logger *slog.Logger) (string, error) { - now := time.Now() - - switch { - case user.SubscriptionEndDate != nil && - user.SubscriptionEndDate.After(now) && - user.SubscriptionStatus != "expired" && - user.SubscriptionCredits > 0: - logger.Info("subscription plan in canUseCredit check") - return "subscription", nil - case user.IndividualCredits > 0: - logger.Info("individual plan in canUseCredit check") - return "individual", nil - default: - logger.Info("no valid credits in canUseCredit check") - return "", ErrNoValidCredits - } -} - -func deductAndLogCredit(user *user.User, userRepo user.UserRepo, billingRepo billing.BillingRepo, logger *slog.Logger) error { - creditType, err := canUseCredit(user, logger) +func (i *InterviewService) deductAndLogCredit(user *user.User) error { + creditType, err := i.canUseCredit(user) if err != nil { - logger.Error("canUseCredit failed", "error", err) + i.Logger.Error("canUseCredit failed", "error", err) return err } if creditType == "" { - logger.Info("user doesn't have a valid plan or credits") + i.Logger.Info("user doesn't have a valid plan or credits") return fmt.Errorf("user doesn't have a valid plan or credits") } - err = userRepo.AddCredits(user.ID, -1, creditType) + err = i.UserRepo.AddCredits(user.ID, -1, creditType) if err != nil { - logger.Error("AddCredits failed", "error", err) + i.Logger.Error("AddCredits failed", "error", err) return err } @@ -136,10 +116,29 @@ func deductAndLogCredit(user *user.User, userRepo user.UserRepo, billingRepo bil CreditType: creditType, Reason: reason, } - if err := billingRepo.LogCreditTransaction(tx); err != nil { - logger.Error("billingRepo.LogCreditTransaction failed", "error", err) + if err := i.BillingRepo.LogCreditTransaction(tx); err != nil { + i.Logger.Error("billingRepo.LogCreditTransaction failed", "error", err) return err } return nil } + +func (i *InterviewService) canUseCredit(user *user.User) (string, error) { + now := time.Now() + + switch { + case user.SubscriptionEndDate != nil && + user.SubscriptionEndDate.After(now) && + user.SubscriptionStatus != "expired" && + user.SubscriptionCredits > 0: + i.Logger.Info("subscription plan in canUseCredit check") + return "subscription", nil + case user.IndividualCredits > 0: + i.Logger.Info("individual plan in canUseCredit check") + return "individual", nil + default: + i.Logger.Info("no valid credits in canUseCredit check") + return "", ErrNoValidCredits + } +}