Skip to content

Commit fdbb489

Browse files
committed
Raise event when phone is updated or deleted
1 parent 07d50b6 commit fdbb489

11 files changed

Lines changed: 146 additions & 24 deletions

File tree

api/pkg/events/message_send_failed_event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const EventTypeMessageSendFailed = "message.send.failed"
1313
// MessageSendFailedPayload is the payload of the EventTypeMessageSendFailed event
1414
type MessageSendFailedPayload struct {
1515
ID uuid.UUID `json:"id"`
16-
ErrorMessage string `json:"errorMessage"`
16+
ErrorMessage string `json:"error_message"`
1717
UserID entities.UserID `json:"user_id"`
1818
Owner string `json:"owner"`
1919
Contact string `json:"contact"`
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
"github.com/google/uuid"
8+
)
9+
10+
// EventTypePhoneDeleted is emitted when the phone os deleted
11+
const EventTypePhoneDeleted = "phone.deleted"
12+
13+
// PhoneDeletedPayload is the payload of the EventTypePhoneDeleted event
14+
type PhoneDeletedPayload struct {
15+
PhoneID uuid.UUID `json:"phone_id"`
16+
UserID entities.UserID `json:"user_id"`
17+
Timestamp time.Time `json:"timestamp"`
18+
Owner string `json:"owner"`
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
"github.com/google/uuid"
8+
)
9+
10+
// EventTypePhoneUpdated is emitted when the phone is updated
11+
const EventTypePhoneUpdated = "phone.updated"
12+
13+
// PhoneUpdatedPayload is the payload of the EventTypePhoneUpdated event
14+
type PhoneUpdatedPayload struct {
15+
PhoneID uuid.UUID `json:"phone_id"`
16+
UserID entities.UserID `json:"user_id"`
17+
Timestamp time.Time `json:"timestamp"`
18+
Owner string `json:"owner"`
19+
}

api/pkg/handlers/phone_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (h *PhoneHandler) Delete(c *fiber.Ctx) error {
159159
return h.responseUnprocessableEntity(c, errors, "validation errors while deleting phone")
160160
}
161161

162-
err := h.service.Delete(ctx, h.userIDFomContext(c), request.PhoneIDUuid())
162+
err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c), request.PhoneIDUuid())
163163
if err != nil {
164164
msg := fmt.Sprintf("cannot delete phones with params [%+#v]", request)
165165
ctxLogger.Error(stacktrace.Propagate(err, msg))

api/pkg/repositories/gorm_phone_repository.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ func NewGormPhoneRepository(
3434
}
3535

3636
// LoadByID loads a phone by ID
37-
func (repository *gormPhoneRepository) LoadByID(ctx context.Context, phoneID uuid.UUID) (*entities.Phone, error) {
37+
func (repository *gormPhoneRepository) LoadByID(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) (*entities.Phone, error) {
3838
ctx, span := repository.tracer.Start(ctx)
3939
defer span.End()
4040

4141
phone := new(entities.Phone)
42-
err := repository.db.WithContext(ctx).First(phone, phoneID).Error
42+
err := repository.db.WithContext(ctx).
43+
Where("user_id = ?", userID).
44+
Where("id = ?", phoneID).
45+
First(&phone).Error
4346
if errors.Is(err, gorm.ErrRecordNotFound) {
4447
msg := fmt.Sprintf("phone with ID [%s] does not exist", phoneID)
4548
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))

api/pkg/repositories/phone_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type PhoneRepository interface {
2020
Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.Phone, error)
2121

2222
// LoadByID a phone by ID
23-
LoadByID(ctx context.Context, phoneID uuid.UUID) (*entities.Phone, error)
23+
LoadByID(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) (*entities.Phone, error)
2424

2525
// Delete an entities.Phone
2626
Delete(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) error

api/pkg/services/phone_notification_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone
6161
ctx, span := service.tracer.Start(ctx)
6262
defer span.End()
6363

64-
phone, err := service.phoneRepository.LoadByID(ctx, params.PhoneID)
64+
phone, err := service.phoneRepository.LoadByID(ctx, params.UserID, params.PhoneID)
6565
if err != nil {
6666
msg := fmt.Sprintf("cannot load phone with userID [%s] and phoneID [%s]", params.UserID, params.PhoneID)
6767
return service.handleNotificationFailed(ctx, errors.New(msg), params)

api/pkg/services/phone_service.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"fmt"
66
"time"
77

8+
"github.com/NdoleStudio/httpsms/pkg/events"
9+
cloudevents "github.com/cloudevents/sdk-go/v2"
10+
811
"github.com/google/uuid"
912
"github.com/nyaruka/phonenumbers"
1013

@@ -17,20 +20,24 @@ import (
1720

1821
// PhoneService is handles phone requests
1922
type PhoneService struct {
23+
service
2024
logger telemetry.Logger
2125
tracer telemetry.Tracer
2226
repository repositories.PhoneRepository
27+
dispatcher *EventDispatcher
2328
}
2429

2530
// NewPhoneService creates a new PhoneService
2631
func NewPhoneService(
2732
logger telemetry.Logger,
2833
tracer telemetry.Tracer,
2934
repository repositories.PhoneRepository,
35+
dispatcher *EventDispatcher,
3036
) (s *PhoneService) {
3137
return &PhoneService{
3238
logger: logger.WithService(fmt.Sprintf("%T", s)),
3339
tracer: tracer,
40+
dispatcher: dispatcher,
3441
repository: repository,
3542
}
3643
}
@@ -58,6 +65,7 @@ type PhoneUpsertParams struct {
5865
FcmToken *string
5966
MessagesPerMinute *uint
6067
MessageExpirationDuration *time.Duration
68+
Source string
6169
UserID entities.UserID
6270
}
6371

@@ -82,40 +90,63 @@ func (service *PhoneService) Upsert(ctx context.Context, params PhoneUpsertParam
8290
msg := fmt.Sprintf("cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)
8391
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
8492
}
85-
86-
ctxLogger.Info(fmt.Sprintf("phone saved with id [%s] in the userRepository", phone.ID))
87-
return phone, nil
88-
}
89-
90-
func (service *PhoneService) update(phone *entities.Phone, params PhoneUpsertParams) *entities.Phone {
91-
if phone.FcmToken != nil {
92-
phone.FcmToken = params.FcmToken
93-
}
94-
if params.MessagesPerMinute != nil {
95-
phone.MessagesPerMinute = *params.MessagesPerMinute
93+
ctxLogger.Info(fmt.Sprintf("phone saved with id [%s] in the phone repository", phone.ID))
94+
95+
event, err := service.createPhoneUpdatedEvent(params.Source, events.PhoneUpdatedPayload{
96+
PhoneID: phone.ID,
97+
UserID: phone.UserID,
98+
Timestamp: phone.UpdatedAt,
99+
Owner: phone.PhoneNumber,
100+
})
101+
if err != nil {
102+
msg := fmt.Sprintf("cannot create event when phone is updated")
103+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
96104
}
97105

98-
if params.MessageExpirationDuration != nil {
99-
phone.MessageExpirationSeconds = uint(params.MessageExpirationDuration.Seconds())
106+
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
107+
msg := fmt.Sprintf("cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)
108+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
100109
}
101-
return phone
110+
111+
return phone, nil
102112
}
103113

104114
// Delete an entities.Phone
105-
func (service *PhoneService) Delete(ctx context.Context, userID entities.UserID, phoneID uuid.UUID) error {
115+
func (service *PhoneService) Delete(ctx context.Context, source string, userID entities.UserID, phoneID uuid.UUID) error {
106116
ctx, span := service.tracer.Start(ctx)
107117
defer span.End()
108118

109119
ctxLogger := service.tracer.CtxLogger(service.logger, span)
110120

111-
err := service.repository.Delete(ctx, userID, phoneID)
121+
phone, err := service.repository.LoadByID(ctx, userID, phoneID)
112122
if err != nil {
123+
msg := fmt.Sprintf("cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID)
124+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
125+
}
126+
127+
if err = service.repository.Delete(ctx, userID, phoneID); err != nil {
113128
msg := fmt.Sprintf("cannot delete phone with id [%s] and user id [%s]", phoneID, userID)
114129
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
115130
}
116131

117132
ctxLogger.Info(fmt.Sprintf("deleted phone with id [%s] and user id [%s]", phoneID, userID))
118133

134+
event, err := service.createPhoneDeletedEvent(source, events.PhoneDeletedPayload{
135+
PhoneID: phone.ID,
136+
UserID: phone.UserID,
137+
Timestamp: phone.UpdatedAt,
138+
Owner: phone.PhoneNumber,
139+
})
140+
if err != nil {
141+
msg := fmt.Sprintf("cannot create event when phone is deleted")
142+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
143+
}
144+
145+
if err = service.dispatcher.Dispatch(ctx, event); err != nil {
146+
msg := fmt.Sprintf("cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)
147+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
148+
}
149+
119150
return nil
120151
}
121152

@@ -141,3 +172,25 @@ func (service *PhoneService) createPhone(ctx context.Context, params PhoneUpsert
141172

142173
return phone, nil
143174
}
175+
176+
func (service *PhoneService) createPhoneUpdatedEvent(source string, payload events.PhoneUpdatedPayload) (cloudevents.Event, error) {
177+
return service.createEvent(events.EventTypePhoneUpdated, source, payload)
178+
}
179+
180+
func (service *PhoneService) createPhoneDeletedEvent(source string, payload events.PhoneDeletedPayload) (cloudevents.Event, error) {
181+
return service.createEvent(events.EventTypePhoneUpdated, source, payload)
182+
}
183+
184+
func (service *PhoneService) update(phone *entities.Phone, params PhoneUpsertParams) *entities.Phone {
185+
if phone.FcmToken != nil {
186+
phone.FcmToken = params.FcmToken
187+
}
188+
if params.MessagesPerMinute != nil {
189+
phone.MessagesPerMinute = *params.MessagesPerMinute
190+
}
191+
192+
if params.MessageExpirationDuration != nil {
193+
phone.MessageExpirationSeconds = uint(params.MessageExpirationDuration.Seconds())
194+
}
195+
return phone
196+
}

api/pkg/services/service.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package services
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
cloudevents "github.com/cloudevents/sdk-go/v2"
8+
"github.com/google/uuid"
9+
"github.com/palantir/stacktrace"
10+
)
11+
12+
type service struct{}
13+
14+
func (service *service) createEvent(eventType string, source string, payload any) (cloudevents.Event, error) {
15+
event := cloudevents.NewEvent()
16+
17+
event.SetSource(source)
18+
event.SetType(eventType)
19+
event.SetTime(time.Now().UTC())
20+
event.SetID(uuid.New().String())
21+
22+
if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil {
23+
msg := fmt.Sprintf("cannot encode %T [%#+v] as JSON", payload, payload)
24+
return event, stacktrace.Propagate(err, msg)
25+
}
26+
27+
return event, nil
28+
}

web/pages/messages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<v-container fluid class="pa-0" :fill-height="$vuetify.breakpoint.lgAndUp">
33
<div class="w-full h-full">
4-
<v-app-bar :dense="$vuetify.breakpoint.mdAndDown">
4+
<v-app-bar height="60" :dense="$vuetify.breakpoint.mdAndDown">
55
<v-btn icon to="/">
66
<v-icon>{{ mdiArrowLeft }}</v-icon>
77
</v-btn>

0 commit comments

Comments
 (0)