Passed
Push — main ( df0a9c...c818dd )
by Acho
01:49
created

services.NewUserService   A

Complexity

Conditions 1

Size

Total Lines 19
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 19
dl 0
loc 19
c 0
b 0
f 0
rs 9.45
nop 8

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