From 44e00684ccd2984b27b0ae1eb22838104a01dd8f Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 10 Nov 2025 16:25:34 +0000 Subject: [PATCH] tsk-20: Add event to scheduled message (#23) Closes #20 Reviewed-on: https://git.kundeng.us/phoenix/textsender-api/pulls/23 Co-authored-by: phoenix Co-committed-by: phoenix --- cmd/api/main.go | 3 + go.mod | 2 +- go.sum | 4 +- internal/handler/endpoint/endpoint.go | 1 + internal/handler/mock_store_test.go | 60 +++++++++++++ internal/handler/scheduled_message_event.go | 81 ++++++++++++++++++ .../handler/scheduled_message_event_test.go | 85 +++++++++++++++++++ .../store/scheduled_message_event_store.go | 47 ++++++++++ internal/store/scheduled_message_store.go | 3 - migrations/schema.sql | 8 ++ 10 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 internal/handler/scheduled_message_event.go create mode 100644 internal/handler/scheduled_message_event_test.go create mode 100644 internal/store/scheduled_message_event_store.go diff --git a/cmd/api/main.go b/cmd/api/main.go index d04eec7..6d43dd0 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -64,10 +64,12 @@ func main() { contactStore := store.NewContactStore(db.Pool) messageStore := store.NewMessageStore(db.Pool) schStore := store.NewScheduledMessageStore(db.Pool) + schMsgEvent := store.NewScheduledMessageEventStore(db.Pool) contactHandler := handler.NewContactHandler(contactStore) messageHandler := handler.NewMessageHandler(messageStore) scheduledMessageHandler := handler.NewScheduledMessageHandler(schStore) + scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(schMsgEvent) router := chi.NewRouter() @@ -81,6 +83,7 @@ func main() { router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) + router.Handle(endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent))) // Start server server := &http.Server{ diff --git a/go.mod b/go.mod index bdc0a60..768b311 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.kundeng.us/phoenix/textsender-api go 1.25.3 require ( - git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556 + git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 0770952..c20e94a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556 h1:dDgjre2FlukxeKZfJiP3gtIT4wyzpQ8glAYV9LpLtuU= -git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= +git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556 h1:2shgOKmglCODswpCJeCuyPiUAph3wfc0Ep1z6b/ifis= +git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index d139e64..acc7803 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -6,3 +6,4 @@ const GET_MESSAGE = "/api/v1/message" const GET_CONTACT = "/api/v1/contact" const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" const ScheduleMessageEndpoint = "/api/v1/schedule/message" +const AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event" diff --git a/internal/handler/mock_store_test.go b/internal/handler/mock_store_test.go index 6860ebd..417159e 100644 --- a/internal/handler/mock_store_test.go +++ b/internal/handler/mock_store_test.go @@ -3,6 +3,7 @@ package handler import ( "context" "errors" + "fmt" "sync" "github.com/google/uuid" @@ -238,3 +239,62 @@ func (m *MockScheduledMessageStore) CreateScheduledMessage(ctx context.Context, m.ScheduledMessagesByKey[key] = schedMsg return nil } + +type ScheduledMessageEventKey struct { + RecipientId uuid.UUID + MessageId uuid.UUID + ScheduledMessageId uuid.UUID +} + +type MockScheduledMessageEventStore struct { + ScheduledMessageEvents map[uuid.UUID]*scheduling.ScheduledMessageEvent + ScheduledMessageEventsByKey map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent + mu sync.RWMutex + Error error // Optional: simulate errors +} + +func NewMockScheduledMessageEventStore() *MockScheduledMessageEventStore { + return &MockScheduledMessageEventStore{ + ScheduledMessageEvents: make(map[uuid.UUID]*scheduling.ScheduledMessageEvent), + ScheduledMessageEventsByKey: make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent), + } +} + +func (m *MockScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return m.Error + } + + if event.Id == uuid.Nil { + event.Id = uuid.New() + } + + if exists, err := m.Exists(ctx, event); err != nil { + return err + } else if exists { + return fmt.Errorf("Already exists") + } else { + key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} + m.ScheduledMessageEvents[event.Id] = event + m.ScheduledMessageEventsByKey[key] = event + return nil + } + +} + +func (m *MockScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return false, m.Error + } + + key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} + _, exists := m.ScheduledMessageEventsByKey[key] + + return exists, nil +} diff --git a/internal/handler/scheduled_message_event.go b/internal/handler/scheduled_message_event.go new file mode 100644 index 0000000..3b1ed49 --- /dev/null +++ b/internal/handler/scheduled_message_event.go @@ -0,0 +1,81 @@ +package handler + +import ( + "net/http" + + "github.com/google/uuid" + + "git.kundeng.us/phoenix/textsender-api/internal/store" + "git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling" +) + +type RequestAddScheduledMessageEvent struct { + RecipientId uuid.UUID `json:"recipient_id"` + MessageId uuid.UUID `json:"message_id"` + ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` +} + +type AddScheduledMessageEventResponse struct { + Message string `json:"message"` + Data []scheduling.ScheduledMessageEvent `json:"data"` +} + +type ScheduledMessageEventHandler struct { + ScheduledMessageEventStore store.ScheduledMessageEventStore +} + +func NewScheduledMessageEventHandler(str store.ScheduledMessageEventStore) *ScheduledMessageEventHandler { + return &ScheduledMessageEventHandler{ScheduledMessageEventStore: str} +} + +func (s *ScheduledMessageEventHandler) AddScheduledMessageEvent(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req RequestAddScheduledMessageEvent + if err := ExtractFromRequest(r, &req); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + } + defer r.Body.Close() + + event := scheduling.ScheduledMessageEvent{RecipientId: req.RecipientId, MessageId: req.MessageId, ScheduledMessageId: req.ScheduledMessageId} + + var statusCode int + var resp AddScheduledMessageEventResponse + + if event.RecipientId == uuid.Nil { + statusCode = http.StatusBadRequest + resp.Message = "Recipient Id is nil" + } else if event.MessageId == uuid.Nil { + statusCode = http.StatusBadRequest + resp.Message = "Message Id is nil" + } else if event.ScheduledMessageId == uuid.Nil { + statusCode = http.StatusBadRequest + resp.Message = "Scheduled Message Id is nil" + } else { + ctx := r.Context() + + if exists, err := s.ScheduledMessageEventStore.Exists(ctx, &event); err == nil { + if exists { + statusCode = http.StatusBadRequest + resp.Message = "Event already present" + } else { + if err = s.ScheduledMessageEventStore.CreateScheduledMessageEvent(ctx, &event); err == nil { + statusCode = http.StatusCreated + resp.Message = "Successful" + resp.Data = append(resp.Data, event) + } else { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } + } + } else { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } + } + + RespondWithJSON(w, statusCode, &resp) +} diff --git a/internal/handler/scheduled_message_event_test.go b/internal/handler/scheduled_message_event_test.go new file mode 100644 index 0000000..8b55181 --- /dev/null +++ b/internal/handler/scheduled_message_event_test.go @@ -0,0 +1,85 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "git.kundeng.us/phoenix/textsender-models/pkg/contact" + "git.kundeng.us/phoenix/textsender-models/pkg/message" + "git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" +) + +type CreateScheduledMessageEventRequest struct { + RecipientId uuid.UUID `json:"recipient_id"` + MessageId uuid.UUID `json:"message_id"` + ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` +} + +func TestCreateScheduledMessageEventWithMock(t *testing.T) { + now := time.Now() + + mockStore := NewMockScheduledMessageEventStore() + contactStore := NewMockContactStore() + messageStore := NewMockMessageStore() + schMsgStore := NewMockScheduledMessageStore() + handler := NewScheduledMessageEventHandler(mockStore) + + recipientId := uuid.New() + messageId := uuid.New() + scheduledMessageId := uuid.New() + testUserId := uuid.New() + + con := contact.Contact{} + con.Id = recipientId + con.PhoneNumber = "+10123456789" + con.UserId = testUserId + + msg := message.Message{} + msg.Id = messageId + msg.Content = "Oh how the might have fallen" + msg.UserId = testUserId + + schMsg := scheduling.ScheduledMessage{} + schMsg.Id = scheduledMessageId + schMsg.UserId = testUserId + schMsg.Scheduled = now.Add(20*time.Minute) + + ctx := t.Context() + + if err := contactStore.CreateContact(ctx, &con); err != nil { + assert.NoError(t, err, "Error creating contact: %v", err) + } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { + assert.NoError(t, err, "Error creating message: %v", err) + } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { + assert.NoError(t, err, "Error creating scheduled message: %v", err) + } + + testReq := CreateScheduledMessageEventRequest{RecipientId: recipientId, MessageId: messageId, ScheduledMessageId: scheduledMessageId} + jsonValue, _ := json.Marshal(testReq) + + req, _ := http.NewRequest("POST", endpoint.AddEventToScheduledMessageEndpoint, strings.NewReader(string(jsonValue))) + rr := httptest.NewRecorder() + + handler.AddScheduledMessageEvent(rr, req) + + assert.Equal(t, http.StatusCreated, rr.Code) + + var response AddScheduledMessageEventResponse + err := json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err, "Error creating event %v", err) + + assert.NotEmpty(t, response.Data, "No event created") + + var event scheduling.ScheduledMessageEvent + event = response.Data[0] + + assert.NotEmpty(t, event.Created, "Created date should not be empty") +} diff --git a/internal/store/scheduled_message_event_store.go b/internal/store/scheduled_message_event_store.go new file mode 100644 index 0000000..297b9d6 --- /dev/null +++ b/internal/store/scheduled_message_event_store.go @@ -0,0 +1,47 @@ +package store + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/pgxpool" + + "git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling" +) + +type ScheduledMessageEventStore interface { + CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error + Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) +} + +type PGScheduledMessageEventStore struct { + db *pgxpool.Pool +} + +func NewScheduledMessageEventStore(db *pgxpool.Pool) *PGScheduledMessageEventStore { + return &PGScheduledMessageEventStore{db: db} +} + +func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { + query := ` + INSERT INTO scheduled_message_events (recipient_id, message_id, scheduled_message_id) + VALUES ($1, $2, $3) + RETURNING id, created + ` + + return s.db.QueryRow(ctx, query, event.RecipientId, event.MessageId, event.ScheduledMessageId).Scan( + &event.Id, &event.Created, + ) +} + +func (s *PGScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) { + query := `SELECT EXISTS(SELECT 1 FROM scheduled_message_events WHERE scheduled_message_id = $1 AND recipient_id = $2)` + + var exists bool + err := s.db.QueryRow(ctx, query, event.ScheduledMessageId, event.RecipientId).Scan(&exists) + if err != nil { + return false, fmt.Errorf("Scheduled message event does not exist: %w", err) + } else { + return exists, nil + } +} diff --git a/internal/store/scheduled_message_store.go b/internal/store/scheduled_message_store.go index 5ac75fb..ea71082 100644 --- a/internal/store/scheduled_message_store.go +++ b/internal/store/scheduled_message_store.go @@ -2,10 +2,7 @@ package store import ( "context" - // "fmt" - // "github.com/google/uuid" - // "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling" diff --git a/migrations/schema.sql b/migrations/schema.sql index 57a4de6..5e0cefe 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -21,3 +21,11 @@ CREATE TABLE scheduled_messages ( status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), user_id UUID NOT NULL ); + +CREATE TABLE scheduled_message_events ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + recipient_id UUID NOT NULL, + message_id UUID NOT NULL, + scheduled_message_id UUID NOT NULL, + created timestamptz DEFAULT now() +)