services.NewUserService   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 21
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
dl 0
loc 21
rs 9.376
c 0
b 0
f 0
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
package services
2
3
import (
4
	"context"
5
	"fmt"
6
	"time"
7
8
	"firebase.google.com/go/auth"
9
10
	"github.com/NdoleStudio/httpsms/pkg/events"
11
12
	"github.com/NdoleStudio/httpsms/pkg/emails"
13
	"github.com/NdoleStudio/lemonsqueezy-go"
14
15
	"github.com/NdoleStudio/httpsms/pkg/repositories"
16
	"github.com/google/uuid"
17
	"github.com/palantir/stacktrace"
18
19
	"github.com/NdoleStudio/httpsms/pkg/entities"
20
	"github.com/NdoleStudio/httpsms/pkg/telemetry"
21
)
22
23
// UserService is handles user requests
24
type UserService struct {
25
	service
26
	logger             telemetry.Logger
27
	tracer             telemetry.Tracer
28
	emailFactory       emails.UserEmailFactory
29
	mailer             emails.Mailer
30
	repository         repositories.UserRepository
31
	dispatcher         *EventDispatcher
32
	marketingService   *MarketingService
33
	authClient         *auth.Client
34
	lemonsqueezyClient *lemonsqueezy.Client
35
}
36
37
// NewUserService creates a new UserService
38
func NewUserService(
39
	logger telemetry.Logger,
40
	tracer telemetry.Tracer,
41
	repository repositories.UserRepository,
42
	mailer emails.Mailer,
43
	emailFactory emails.UserEmailFactory,
44
	marketingService *MarketingService,
45
	lemonsqueezyClient *lemonsqueezy.Client,
46
	dispatcher *EventDispatcher,
47
	authClient *auth.Client,
48
) (s *UserService) {
49
	return &UserService{
50
		logger:             logger.WithService(fmt.Sprintf("%T", s)),
51
		tracer:             tracer,
52
		mailer:             mailer,
53
		marketingService:   marketingService,
54
		emailFactory:       emailFactory,
55
		repository:         repository,
56
		dispatcher:         dispatcher,
57
		authClient:         authClient,
58
		lemonsqueezyClient: lemonsqueezyClient,
59
	}
60
}
61
62
// Get fetches or creates an entities.User
63
func (service *UserService) Get(ctx context.Context, authUser entities.AuthContext) (*entities.User, error) {
64
	ctx, span := service.tracer.Start(ctx)
65
	defer span.End()
66
67
	user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
68
	if err != nil {
69
		msg := fmt.Sprintf("could not get [%T] with from [%+#v]", user, authUser)
70
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
71
	}
72
73
	if isNew {
74
		service.marketingService.AddToList(ctx, user)
75
	}
76
77
	return user, nil
78
}
79
80
// GetByID fetches an entities.User
81
func (service *UserService) GetByID(ctx context.Context, userID entities.UserID) (*entities.User, error) {
82
	ctx, span, _ := service.tracer.StartWithLogger(ctx, service.logger)
83
	defer span.End()
84
85
	user, err := service.repository.Load(ctx, userID)
86
	if err != nil {
87
		msg := fmt.Sprintf("could not get [%T] with ID [%s]", user, userID)
88
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
89
	}
90
91
	return user, nil
92
}
93
94
// UserUpdateParams are parameters for updating an entities.User
95
type UserUpdateParams struct {
96
	Timezone      *time.Location
97
	ActivePhoneID *uuid.UUID
98
}
99
100
// Update an entities.User
101
func (service *UserService) Update(ctx context.Context, authUser entities.AuthContext, params UserUpdateParams) (*entities.User, error) {
102
	ctx, span := service.tracer.Start(ctx)
103
	defer span.End()
104
105
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
106
107
	user, isNew, err := service.repository.LoadOrStore(ctx, authUser)
108
	if err != nil {
109
		msg := fmt.Sprintf("could not get [%T] with from [%+#v]", user, authUser)
110
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
111
	}
112
113
	if isNew {
114
		service.marketingService.AddToList(ctx, user)
115
	}
116
117
	user.Timezone = params.Timezone.String()
118
	user.ActivePhoneID = params.ActivePhoneID
119
120
	if err = service.repository.Update(ctx, user); err != nil {
121
		msg := fmt.Sprintf("cannot save user with id [%s]", user.ID)
122
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
123
	}
124
125
	ctxLogger.Info(fmt.Sprintf("user saved with id [%s] in the userRepository", user.ID))
126
	return user, nil
127
}
128
129
// UserNotificationUpdateParams are parameters for updating the notifications of a user
130
type UserNotificationUpdateParams struct {
131
	MessageStatusEnabled bool
132
	WebhookEnabled       bool
133
	HeartbeatEnabled     bool
134
	NewsletterEnabled    bool
135
}
136
137
// UpdateNotificationSettings for an entities.User
138
func (service *UserService) UpdateNotificationSettings(ctx context.Context, userID entities.UserID, params *UserNotificationUpdateParams) (*entities.User, error) {
139
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
140
	defer span.End()
141
142
	user, err := service.repository.Load(ctx, userID)
143
	if err != nil {
144
		msg := fmt.Sprintf("could not load [%T] with ID [%s]", user, userID)
145
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
146
	}
147
148
	user.NotificationWebhookEnabled = params.WebhookEnabled
149
	user.NotificationHeartbeatEnabled = params.HeartbeatEnabled
150
	user.NotificationMessageStatusEnabled = params.MessageStatusEnabled
151
	user.NotificationNewsletterEnabled = params.NewsletterEnabled
152
153
	if err = service.repository.Update(ctx, user); err != nil {
154
		msg := fmt.Sprintf("cannot save user with id [%s] in [%T]", user.ID, service.repository)
155
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
156
	}
157
158
	ctxLogger.Info(fmt.Sprintf("updated notification settings for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
159
	return user, nil
160
}
161
162
// RotateAPIKey for an entities.User
163
func (service *UserService) RotateAPIKey(ctx context.Context, source string, userID entities.UserID) (*entities.User, error) {
164
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
165
	defer span.End()
166
167
	user, err := service.repository.RotateAPIKey(ctx, userID)
168
	if err != nil {
169
		msg := fmt.Sprintf("could not rotate API key for [%T] with ID [%s]", user, userID)
170
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
171
	}
172
173
	ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
174
175
	event, err := service.createEvent(events.UserAPIKeyRotated, source, &events.UserAPIKeyRotatedPayload{
176
		UserID:    user.ID,
177
		Email:     user.Email,
178
		Timestamp: time.Now().UTC(),
179
		Timezone:  user.Timezone,
180
	})
181
	if err != nil {
182
		msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)
183
		ctxLogger.Error(stacktrace.Propagate(err, msg))
184
		return user, nil
185
	}
186
187
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
188
		msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)
189
		ctxLogger.Error(stacktrace.Propagate(err, msg))
190
		return user, nil
191
	}
192
193
	return user, nil
194
}
195
196
// Delete an entities.User
197
func (service *UserService) Delete(ctx context.Context, source string, userID entities.UserID) error {
198
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
199
	defer span.End()
200
201
	user, err := service.repository.Load(ctx, userID)
202
	if err != nil {
203
		msg := fmt.Sprintf("cannot load user with ID [%s] from the [%T]", userID, service.repository)
204
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
205
	}
206
207
	if !user.IsOnFreePlan() && user.SubscriptionRenewsAt != nil && user.SubscriptionRenewsAt.After(time.Now()) {
208
		msg := fmt.Sprintf("cannot delete user with ID [%s] because they are have an active [%s] subscription which renews at [%s]", userID, user.SubscriptionName, user.SubscriptionRenewsAt)
209
		return service.tracer.WrapErrorSpan(span, stacktrace.NewError(msg))
210
	}
211
212
	if err = service.repository.Delete(ctx, user); err != nil {
213
		msg := fmt.Sprintf("could not delete user with ID [%s] from the [%T]", userID, service.repository)
214
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
215
	}
216
217
	ctxLogger.Info(fmt.Sprintf("sucessfully deleted user with ID [%s] in the [%T]", userID, service.repository))
218
219
	event, err := service.createEvent(events.UserAccountDeleted, source, &events.UserAccountDeletedPayload{
220
		UserID:    userID,
221
		Timestamp: time.Now().UTC(),
222
	})
223
	if err != nil {
224
		msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID)
225
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
226
	}
227
228
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
229
		msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), userID)
230
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
231
	}
232
233
	return nil
234
}
235
236
// SendAPIKeyRotatedEmail sends an email to an entities.User when the API key is rotated
237
func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload *events.UserAPIKeyRotatedPayload) error {
238
	ctx, span := service.tracer.Start(ctx)
239
	defer span.End()
240
241
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
242
243
	email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone)
244
	if err != nil {
245
		msg := fmt.Sprintf("cannot create api key rotated email for user [%s]", payload.UserID)
246
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
247
	}
248
249
	if err = service.mailer.Send(ctx, email); err != nil {
250
		msg := fmt.Sprintf("canot create api key rotated email to user [%s]", payload.UserID)
251
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
252
	}
253
254
	ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID  [%s]", payload.Email, payload.UserID))
255
	return nil
256
}
257
258
// UserSendPhoneDeadEmailParams are parameters for notifying a user when a phone is dead
259
type UserSendPhoneDeadEmailParams struct {
260
	UserID                 entities.UserID
261
	PhoneID                uuid.UUID
262
	Owner                  string
263
	LastHeartbeatTimestamp time.Time
264
}
265
266
// SendPhoneDeadEmail sends an email to an entities.User when a phone is dead
267
func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *UserSendPhoneDeadEmailParams) error {
268
	ctx, span := service.tracer.Start(ctx)
269
	defer span.End()
270
271
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
272
273
	user, err := service.repository.Load(ctx, params.UserID)
274
	if err != nil {
275
		msg := fmt.Sprintf("could not get [%T] with ID [%s]", user, params.UserID)
276
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
277
	}
278
279
	if !user.NotificationHeartbeatEnabled {
280
		ctxLogger.Info(fmt.Sprintf("[%s] email notifications disabled for user [%s] with owner [%s]", events.EventTypePhoneHeartbeatOffline, params.UserID, params.Owner))
281
		return nil
282
	}
283
284
	email, err := service.emailFactory.PhoneDead(user, params.LastHeartbeatTimestamp, params.Owner)
285
	if err != nil {
286
		msg := fmt.Sprintf("cannot create phone dead email for user [%s]", params.UserID)
287
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
288
	}
289
290
	if err = service.mailer.Send(ctx, email); err != nil {
291
		msg := fmt.Sprintf("canot send phone dead notification to user [%s]", params.UserID)
292
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
293
	}
294
295
	ctxLogger.Info(fmt.Sprintf("phone dead notification sent successfully to [%s] about [%s]", user.Email, params.Owner))
296
	return nil
297
}
298
299
// StartSubscription starts a subscription for an entities.User
300
func (service *UserService) StartSubscription(ctx context.Context, params *events.UserSubscriptionCreatedPayload) error {
301
	ctx, span := service.tracer.Start(ctx)
302
	defer span.End()
303
304
	user, err := service.repository.Load(ctx, params.UserID)
305
	if err != nil {
306
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
307
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
308
	}
309
310
	user.SubscriptionID = &params.SubscriptionID
311
	user.SubscriptionName = params.SubscriptionName
312
	user.SubscriptionRenewsAt = &params.SubscriptionRenewsAt
313
	user.SubscriptionStatus = &params.SubscriptionStatus
314
	user.SubscriptionEndsAt = nil
315
316
	if err = service.repository.Update(ctx, user); err != nil {
317
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after update", user, params.UserID)
318
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
319
	}
320
321
	return nil
322
}
323
324
// InitiateSubscriptionCancel initiates the cancelling of a subscription on lemonsqueezy
325
func (service *UserService) InitiateSubscriptionCancel(ctx context.Context, userID entities.UserID) error {
326
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
327
	defer span.End()
328
329
	user, err := service.repository.Load(ctx, userID)
330
	if err != nil {
331
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, userID)
332
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
333
	}
334
335
	if _, _, err = service.lemonsqueezyClient.Subscriptions.Cancel(ctx, *user.SubscriptionID); err != nil {
336
		msg := fmt.Sprintf("could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)
337
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
338
	}
339
340
	ctxLogger.Info(fmt.Sprintf("cancelled subscription [%s] for user [%s]", *user.SubscriptionID, user.ID))
341
	return nil
342
}
343
344
// GetSubscriptionUpdateURL initiates the cancelling of a subscription on lemonsqueezy
345
func (service *UserService) GetSubscriptionUpdateURL(ctx context.Context, userID entities.UserID) (url string, err error) {
346
	ctx, span := service.tracer.Start(ctx)
347
	defer span.End()
348
349
	user, err := service.repository.Load(ctx, userID)
350
	if err != nil {
351
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, userID)
352
		return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
353
	}
354
355
	subscription, _, err := service.lemonsqueezyClient.Subscriptions.Get(ctx, *user.SubscriptionID)
356
	if err != nil {
357
		msg := fmt.Sprintf("could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)
358
		return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
359
	}
360
361
	return subscription.Data.Attributes.Urls.CustomerPortal, nil
362
}
363
364
// CancelSubscription starts a subscription for an entities.User
365
func (service *UserService) CancelSubscription(ctx context.Context, params *events.UserSubscriptionCancelledPayload) error {
366
	ctx, span := service.tracer.Start(ctx)
367
	defer span.End()
368
369
	user, err := service.repository.Load(ctx, params.UserID)
370
	if err != nil {
371
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
372
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
373
	}
374
375
	user.SubscriptionID = &params.SubscriptionID
376
	user.SubscriptionName = params.SubscriptionName
377
	user.SubscriptionRenewsAt = nil
378
	user.SubscriptionStatus = &params.SubscriptionStatus
379
	user.SubscriptionEndsAt = &params.SubscriptionEndsAt
380
381
	if err = service.repository.Update(ctx, user); err != nil {
382
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after update", user, params.UserID)
383
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
384
	}
385
386
	return nil
387
}
388
389
// ExpireSubscription finishes a subscription for an entities.User
390
func (service *UserService) ExpireSubscription(ctx context.Context, params *events.UserSubscriptionExpiredPayload) error {
391
	ctx, span := service.tracer.Start(ctx)
392
	defer span.End()
393
394
	user, err := service.repository.Load(ctx, params.UserID)
395
	if err != nil {
396
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
397
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
398
	}
399
400
	user.SubscriptionID = nil
401
	user.SubscriptionName = entities.SubscriptionNameFree
402
	user.SubscriptionRenewsAt = nil
403
	user.SubscriptionStatus = nil
404
	user.SubscriptionEndsAt = nil
405
406
	if err = service.repository.Update(ctx, user); err != nil {
407
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID)
408
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
409
	}
410
411
	return nil
412
}
413
414
// UpdateSubscription updates a subscription for an entities.User
415
func (service *UserService) UpdateSubscription(ctx context.Context, params *events.UserSubscriptionUpdatedPayload) error {
416
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
417
	defer span.End()
418
419
	user, err := service.repository.Load(ctx, params.UserID)
420
	if err != nil {
421
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
422
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
423
	}
424
425
	if params.SubscriptionStatus != "active" {
426
		msg := fmt.Sprintf("subscription status is [%s] for [%T] with with ID [%s]", params.SubscriptionStatus, user, params.UserID)
427
		ctxLogger.Info(msg)
428
		return nil
429
	}
430
431
	user.SubscriptionID = &params.SubscriptionID
432
	user.SubscriptionName = params.SubscriptionName
433
	user.SubscriptionEndsAt = params.SubscriptionEndsAt
434
	user.SubscriptionRenewsAt = &params.SubscriptionRenewsAt
435
	user.SubscriptionStatus = &params.SubscriptionStatus
436
437
	if err = service.repository.Update(ctx, user); err != nil {
438
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after subscription update", user, params.UserID)
439
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
440
	}
441
442
	return nil
443
}
444
445
// DeleteAuthUser deletes an entities.AuthContext from firebase
446
func (service *UserService) DeleteAuthUser(ctx context.Context, userID entities.UserID) error {
447
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
448
	defer span.End()
449
450
	if err := service.authClient.DeleteUser(ctx, userID.String()); err != nil {
451
		msg := fmt.Sprintf("could not delete [entities.AuthContext] from firebase with ID [%s]", userID)
452
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
453
	}
454
455
	ctxLogger.Info(fmt.Sprintf("deleted [entities.AuthContext] from firebase for user with ID [%s]", userID))
456
	return nil
457
}
458