tsk-44: Record when message was sent (#48)

Closes #44

Reviewed-on: phoenix/textsender-api#48
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-12-01 21:29:31 +00:00
committed by phoenix
parent 34262cda68
commit fddf0f7f87
13 changed files with 308 additions and 44 deletions
+1
View File
@@ -12,3 +12,4 @@ const GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event"
const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}"
const UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update"
const FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch"
const RecordEventResponse = "/api/v1/schedule/message/event/response/record"
@@ -0,0 +1,75 @@
package handler
import (
"net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/types"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
type EventResponseHandler struct {
MessageEventResponseStore store.MessageEventResponseStore
}
func NewEventResponseHandler(str store.MessageEventResponseStore) *EventResponseHandler {
return &EventResponseHandler{MessageEventResponseStore: str}
}
type RecordEventRequest struct {
ScheduledMessageEventId uuid.UUID `json:"scheduled_message_event_id"`
Response types.JSONB `json:"response"`
UserId uuid.UUID `json:"user_id"`
Sent *time.Time `json:"sent,omitempty"`
}
type RecordEventResponse struct {
Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"`
}
func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Request) {
var req RecordEventRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var rp message.MessageEventResponse
var resp RecordEventResponse
if req.Sent == nil {
statusCode = http.StatusBadRequest
resp.Message = "Sent time is empty"
} else if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id is empty"
} else if len(req.Response) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "Response is empty"
} else if req.ScheduledMessageEventId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled message event Id is empty"
} else {
rp.ScheduledMessageEventId = req.ScheduledMessageEventId
rp.Response = req.Response
rp.UserId = req.UserId
rp.Sent = *req.Sent
ctx := r.Context()
if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &rp)
}
}
RespondWithJSON(w, statusCode, &resp)
}
@@ -0,0 +1,82 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
)
func TestRecordMessageEventResponseWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageEventStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
merStore := mock.NewMockMessageEventResponseStore()
handler := NewEventResponseHandler(merStore)
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := scheduling.ScheduledMessageEvent{}
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)
}
event.MessageId = msg.Id
event.RecipientId = con.Id
event.ScheduledMessageId = schMsg.Id
if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
}
sent := now.Add(30 * time.Minute)
bytes := []byte("{\"body\":\"Whoknows?\",\"num_segments\":\"0\",\"direction\":\"outbound-api\",\"from\":\"+12243026041\",\"to\":\"+16303831708\",\"date_updated\":\"Sat,29Nov202519:06:59+0000\",\"uri\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a.json\",\"account_sid\":\"ACefa1ef516314c9d1a68cbd657de49277\",\"num_media\":\"0\",\"status\":\"scheduled\",\"messaging_service_sid\":\"MG803f3676706b92eb02e18dd820c447f2\",\"sid\":\"SM1193a529e7f7a840667cd1e0f13ea95a\",\"date_created\":\"Sat,29Nov202519:06:59+0000\",\"api_version\":\"2010-04-01\",\"subresource_uris\":{\"media\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a/Media.json\"}}")
testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: bytes, UserId: schMsg.UserId, Sent: &sent}
jsonValue, _ := json.Marshal(testReq)
req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.RecordResponse(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response RecordEventResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error parsing message event response:%v", err)
assert.NotEmpty(t, response.Data, "No message event response created")
var msgEventResp message.MessageEventResponse
msgEventResp = *response.Data[0]
assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty")
assert.Equal(t, msgEventResp.Response, testReq.Response, "Responses do not match")
}
@@ -0,0 +1,30 @@
package store
import (
"context"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/jackc/pgx/v5/pgxpool"
)
type MessageEventResponseStore interface {
Create(ctx context.Context, mer *message.MessageEventResponse) error
}
type PGMessageEventResponseStore struct {
db *pgxpool.Pool
}
func NewMessageEventResponseStore(db *pgxpool.Pool) *PGMessageEventResponseStore {
return &PGMessageEventResponseStore{db: db}
}
func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error {
query := `
INSERT INTO message_event_responses (scheduled_message_event_id, response, user_id, sent)
VALUES ($1, $2, $3, $4)
RETURNING id
`
return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id)
}
@@ -0,0 +1,59 @@
package mock
import (
"context"
"fmt"
"sync"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
)
type MessageEventResponseKey struct {
ScheduledMessageEventId uuid.UUID
UserId uuid.UUID
}
type MockMessageEventResponseStore struct {
MessageEventResponses map[uuid.UUID]*message.MessageEventResponse
MessageEventResponsesByKey map[MessageEventResponseKey]*message.MessageEventResponse
mu sync.RWMutex
Error error
}
func NewMockMessageEventResponseStore() *MockMessageEventResponseStore {
return &MockMessageEventResponseStore{
MessageEventResponses: make(map[uuid.UUID]*message.MessageEventResponse),
MessageEventResponsesByKey: make(map[MessageEventResponseKey]*message.MessageEventResponse),
}
}
func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if mer.Id == uuid.Nil {
mer.Id = uuid.New()
}
if len(mer.Response) == 0 {
return fmt.Errorf("Response is empty")
} else if mer.ScheduledMessageEventId == uuid.Nil {
return fmt.Errorf("Scheduled message event Id is nil")
} else if mer.UserId == uuid.Nil {
return fmt.Errorf("User Id is nil")
} else {
key := MessageEventResponseKey{ScheduledMessageEventId: mer.ScheduledMessageEventId, UserId: mer.UserId}
sent := time.Now()
mer.Sent = sent
m.MessageEventResponses[mer.Id] = mer
m.MessageEventResponsesByKey[key] = mer
return nil
}
}