api/pkg/services/phone_service.go   A
last analyzed

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 34
eloc 192
dl 0
loc 298
rs 9.68
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A services.NewPhoneService 0 11 1
A services.*PhoneService.Index 0 14 2
A services.*PhoneService.DeleteAllForUser 0 11 2
A services.*PhoneService.Load 0 5 1
A services.*PhoneService.dispatchPhoneUpdatedEvent 0 22 3
B services.*PhoneService.Delete 0 37 5
A services.*PhoneService.UpsertFCMToken 0 24 4
B services.*PhoneService.update 0 23 8
A services.*PhoneService.createPhone 0 27 2
A services.*PhoneService.createPhoneDeletedEvent 0 2 1
A services.*PhoneService.Upsert 0 36 4
A services.*PhoneService.createPhoneUpdatedEvent 0 2 1
1
package services
2
3
import (
4
	"context"
5
	"fmt"
6
	"time"
7
8
	"github.com/NdoleStudio/httpsms/pkg/events"
9
	cloudevents "github.com/cloudevents/sdk-go/v2"
10
11
	"github.com/google/uuid"
12
	"github.com/nyaruka/phonenumbers"
13
14
	"github.com/NdoleStudio/httpsms/pkg/repositories"
15
	"github.com/palantir/stacktrace"
16
17
	"github.com/NdoleStudio/httpsms/pkg/entities"
18
	"github.com/NdoleStudio/httpsms/pkg/telemetry"
19
)
20
21
// PhoneService is handles phone requests
22
type PhoneService struct {
23
	service
24
	logger     telemetry.Logger
25
	tracer     telemetry.Tracer
26
	repository repositories.PhoneRepository
27
	dispatcher *EventDispatcher
28
}
29
30
// NewPhoneService creates a new PhoneService
31
func NewPhoneService(
32
	logger telemetry.Logger,
33
	tracer telemetry.Tracer,
34
	repository repositories.PhoneRepository,
35
	dispatcher *EventDispatcher,
36
) (s *PhoneService) {
37
	return &PhoneService{
38
		logger:     logger.WithService(fmt.Sprintf("%T", s)),
39
		tracer:     tracer,
40
		dispatcher: dispatcher,
41
		repository: repository,
42
	}
43
}
44
45
// DeleteAllForUser deletes all entities.Phone for an entities.UserID.
46
func (service *PhoneService) DeleteAllForUser(ctx context.Context, userID entities.UserID) error {
47
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
48
	defer span.End()
49
50
	if err := service.repository.DeleteAllForUser(ctx, userID); err != nil {
51
		msg := fmt.Sprintf("could not delete all [entities.Phone] for user with ID [%s]", userID)
52
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
53
	}
54
55
	ctxLogger.Info(fmt.Sprintf("deleted all [entities.Phone] for user with ID [%s]", userID))
56
	return nil
57
}
58
59
// Index fetches the heartbeats for a phone number
60
func (service *PhoneService) Index(ctx context.Context, authUser entities.AuthContext, params repositories.IndexParams) (*[]entities.Phone, error) {
61
	ctx, span := service.tracer.Start(ctx)
62
	defer span.End()
63
64
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
65
66
	phones, err := service.repository.Index(ctx, authUser.ID, params)
67
	if err != nil {
68
		msg := fmt.Sprintf("could not fetch phones with parms [%+#v]", params)
69
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
70
	}
71
72
	ctxLogger.Info(fmt.Sprintf("fetched [%d] phones with prams [%+#v]", len(*phones), params))
73
	return phones, nil
74
}
75
76
// Load a phone by userID and owner
77
func (service *PhoneService) Load(ctx context.Context, userID entities.UserID, owner string) (*entities.Phone, error) {
78
	ctx, span := service.tracer.Start(ctx)
79
	defer span.End()
80
81
	return service.repository.Load(ctx, userID, owner)
82
}
83
84
// PhoneUpsertParams are parameters for creating a new entities.Phone
85
type PhoneUpsertParams struct {
86
	PhoneNumber               *phonenumbers.PhoneNumber
87
	FcmToken                  *string
88
	MessagesPerMinute         *uint
89
	MaxSendAttempts           *uint
90
	WebhookURL                *string
91
	MessageExpirationDuration *time.Duration
92
	MissedCallAutoReply       *string
93
	SIM                       entities.SIM
94
	Source                    string
95
	UserID                    entities.UserID
96
}
97
98
// Upsert a new entities.Phone
99
func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertParams) (*entities.Phone, error) {
100
	ctx, span := service.tracer.Start(ctx)
101
	defer span.End()
102
103
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
104
105
	phone, err := service.repository.Load(ctx, params.UserID, phonenumbers.Format(params.PhoneNumber, phonenumbers.E164))
106
	if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
107
		return service.createPhone(ctx, &PhoneFCMTokenParams{
108
			Source:        params.Source,
109
			PhoneNumber:   params.PhoneNumber,
110
			PhoneAPIKeyID: nil,
111
			UserID:        params.UserID,
112
			FcmToken:      params.FcmToken,
113
			SIM:           params.SIM,
114
		})
115
	}
116
117
	if err != nil {
118
		msg := fmt.Sprintf("cannot upsert phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)
119
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
120
	}
121
122
	if err = service.repository.Save(ctx, service.update(phone, params)); err != nil {
123
		msg := fmt.Sprintf("cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)
124
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
125
	}
126
127
	ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
128
	return phone, service.dispatchPhoneUpdatedEvent(ctx, phone, &PhoneFCMTokenParams{
129
		Source:        params.Source,
130
		PhoneNumber:   params.PhoneNumber,
131
		PhoneAPIKeyID: nil,
132
		UserID:        params.UserID,
133
		FcmToken:      params.FcmToken,
134
		SIM:           params.SIM,
135
	})
136
}
137
138
func (service *PhoneService) dispatchPhoneUpdatedEvent(ctx context.Context, phone *entities.Phone, input *PhoneFCMTokenParams) error {
139
	ctx, span := service.tracer.Start(ctx)
140
	defer span.End()
141
142
	event, err := service.createPhoneUpdatedEvent(input.Source, events.PhoneUpdatedPayload{
143
		PhoneID:       phone.ID,
144
		UserID:        phone.UserID,
145
		Timestamp:     phone.UpdatedAt,
146
		PhoneAPIKeyID: input.PhoneAPIKeyID,
147
		Owner:         phone.PhoneNumber,
148
		SIM:           phone.SIM,
149
	})
150
	if err != nil {
151
		msg := fmt.Sprintf("cannot create event when phone [%s] is updated for user [%s]", phone.ID, phone.UserID)
152
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
153
	}
154
155
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
156
		msg := fmt.Sprintf("cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)
157
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
158
	}
159
	return nil
160
}
161
162
// Delete an entities.Phone
163
func (service *PhoneService) Delete(ctx context.Context, source string, userID entities.UserID, phoneID uuid.UUID) error {
164
	ctx, span := service.tracer.Start(ctx)
165
	defer span.End()
166
167
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
168
169
	phone, err := service.repository.LoadByID(ctx, userID, phoneID)
170
	if err != nil {
171
		msg := fmt.Sprintf("cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID)
172
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
173
	}
174
175
	if err = service.repository.Delete(ctx, userID, phoneID); err != nil {
176
		msg := fmt.Sprintf("cannot delete phone with id [%s] and user id [%s]", phoneID, userID)
177
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
178
	}
179
180
	ctxLogger.Info(fmt.Sprintf("deleted phone with id [%s] and user id [%s]", phoneID, userID))
181
182
	event, err := service.createPhoneDeletedEvent(source, events.PhoneDeletedPayload{
183
		PhoneID:   phone.ID,
184
		UserID:    phone.UserID,
185
		Timestamp: phone.UpdatedAt,
186
		Owner:     phone.PhoneNumber,
187
		SIM:       phone.SIM,
188
	})
189
	if err != nil {
190
		msg := "cannot create event when phone is deleted"
191
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
192
	}
193
194
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
195
		msg := fmt.Sprintf("cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)
196
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
197
	}
198
199
	return nil
200
}
201
202
// PhoneFCMTokenParams are parameters for upserting an entities.Phone
203
type PhoneFCMTokenParams struct {
204
	Source        string
205
	PhoneNumber   *phonenumbers.PhoneNumber
206
	PhoneAPIKeyID *uuid.UUID
207
	UserID        entities.UserID
208
	FcmToken      *string
209
	SIM           entities.SIM
210
}
211
212
// UpsertFCMToken the FCM token for an entities.Phone
213
func (service *PhoneService) UpsertFCMToken(ctx context.Context, params *PhoneFCMTokenParams) (*entities.Phone, error) {
214
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
215
	defer span.End()
216
217
	phone, err := service.repository.Load(ctx, params.UserID, phonenumbers.Format(params.PhoneNumber, phonenumbers.E164))
218
	if stacktrace.GetCode(err) == repositories.ErrCodeNotFound {
219
		return service.createPhone(ctx, params)
220
	}
221
222
	if err != nil {
223
		msg := fmt.Sprintf("cannot upsert FCM token for user with id [%s] and number [%s]", params.UserID, params.PhoneNumber)
224
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
225
	}
226
227
	phone.FcmToken = params.FcmToken
228
	phone.SIM = params.SIM
229
230
	if err = service.repository.Save(ctx, phone); err != nil {
231
		msg := fmt.Sprintf("cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)
232
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
233
	}
234
235
	ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
236
	return phone, service.dispatchPhoneUpdatedEvent(ctx, phone, params)
237
}
238
239
func (service *PhoneService) createPhone(ctx context.Context, params *PhoneFCMTokenParams) (*entities.Phone, error) {
240
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
241
	defer span.End()
242
243
	phone := &entities.Phone{
244
		ID:       uuid.New(),
245
		UserID:   params.UserID,
246
		FcmToken: params.FcmToken,
247
		// Android has a limit of 30 SMS messages per minute without user permission, to be safe let's use 10 messages per minute
248
		// https://android.googlesource.com/platform/frameworks/opt/telephony/+/master/src/java/com/android/internal/telephony/SmsUsageMonitor.java#80
249
		MessagesPerMinute:        10,
250
		MessageExpirationSeconds: 10 * 60, // 10 minutes
251
		MaxSendAttempts:          2,
252
		SIM:                      params.SIM,
253
		MissedCallAutoReply:      nil,
254
		PhoneNumber:              phonenumbers.Format(params.PhoneNumber, phonenumbers.E164),
255
		CreatedAt:                time.Now().UTC(),
256
		UpdatedAt:                time.Now().UTC(),
257
	}
258
259
	if err := service.repository.Save(ctx, phone); err != nil {
260
		msg := fmt.Sprintf("cannot create phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)
261
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
262
	}
263
264
	ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID))
265
	return phone, service.dispatchPhoneUpdatedEvent(ctx, phone, params)
266
}
267
268
func (service *PhoneService) createPhoneUpdatedEvent(source string, payload events.PhoneUpdatedPayload) (cloudevents.Event, error) {
269
	return service.createEvent(events.EventTypePhoneUpdated, source, payload)
270
}
271
272
func (service *PhoneService) createPhoneDeletedEvent(source string, payload events.PhoneDeletedPayload) (cloudevents.Event, error) {
273
	return service.createEvent(events.EventTypePhoneDeleted, source, payload)
274
}
275
276
func (service *PhoneService) update(phone *entities.Phone, params *PhoneUpsertParams) *entities.Phone {
277
	if phone.FcmToken != nil {
278
		phone.FcmToken = params.FcmToken
279
	}
280
	if params.MessagesPerMinute != nil && *params.MessagesPerMinute > 0 {
281
		phone.MessagesPerMinute = *params.MessagesPerMinute
282
	}
283
284
	if params.MaxSendAttempts != nil && *params.MaxSendAttempts > 0 {
285
		phone.MaxSendAttempts = *params.MaxSendAttempts
286
	}
287
288
	if params.MessageExpirationDuration != nil {
289
		phone.MessageExpirationSeconds = uint(params.MessageExpirationDuration.Seconds())
290
	}
291
292
	if params.MissedCallAutoReply != nil {
293
		phone.MissedCallAutoReply = params.MissedCallAutoReply
294
	}
295
296
	phone.SIM = params.SIM
297
298
	return phone
299
}
300