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
+ }
+}