Passed
Push — main ( 570b65...2ff189 )
by Acho
02:48
created

services.NewUserService   A

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.AuthUser) (*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.AuthUser, 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
}
135
136
// UpdateNotificationSettings for an entities.User
137
func (service *UserService) UpdateNotificationSettings(ctx context.Context, userID entities.UserID, params *UserNotificationUpdateParams) (*entities.User, error) {
138
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
139
	defer span.End()
140
141
	user, err := service.repository.Load(ctx, userID)
142
	if err != nil {
143
		msg := fmt.Sprintf("could not load [%T] with ID [%s]", user, userID)
144
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
145
	}
146
147
	user.NotificationWebhookEnabled = params.WebhookEnabled
148
	user.NotificationHeartbeatEnabled = params.HeartbeatEnabled
149
	user.NotificationMessageStatusEnabled = params.MessageStatusEnabled
150
151
	if err = service.repository.Update(ctx, user); err != nil {
152
		msg := fmt.Sprintf("cannot save user with id [%s] in [%T]", user.ID, service.repository)
153
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
154
	}
155
156
	ctxLogger.Info(fmt.Sprintf("updated notification settings for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
157
	return user, nil
158
}
159
160
// RotateAPIKey for an entities.User
161
func (service *UserService) RotateAPIKey(ctx context.Context, source string, userID entities.UserID) (*entities.User, error) {
162
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
163
	defer span.End()
164
165
	user, err := service.repository.RotateAPIKey(ctx, userID)
166
	if err != nil {
167
		msg := fmt.Sprintf("could not rotate API key for [%T] with ID [%s]", user, userID)
168
		return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
169
	}
170
171
	ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository))
172
173
	event, err := service.createEvent(events.UserAPIKeyRotated, source, &events.UserAPIKeyRotatedPayload{
174
		UserID:    user.ID,
175
		Email:     user.Email,
176
		Timestamp: time.Now().UTC(),
177
		Timezone:  user.Timezone,
178
	})
179
	if err != nil {
180
		msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)
181
		ctxLogger.Error(stacktrace.Propagate(err, msg))
182
		return user, nil
183
	}
184
185
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
186
		msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)
187
		ctxLogger.Error(stacktrace.Propagate(err, msg))
188
		return user, nil
189
	}
190
191
	return user, nil
192
}
193
194
// Delete an entities.User
195
func (service *UserService) Delete(ctx context.Context, source string, userID entities.UserID) error {
196
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
197
	defer span.End()
198
199
	user, err := service.repository.Load(ctx, userID)
200
	if err != nil {
201
		msg := fmt.Sprintf("cannot load user with ID [%s] from the [%T]", userID, service.repository)
202
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
203
	}
204
205
	if !user.IsOnFreePlan() && user.SubscriptionRenewsAt != nil && user.SubscriptionRenewsAt.After(time.Now()) {
206
		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)
207
		return service.tracer.WrapErrorSpan(span, stacktrace.NewError(msg))
208
	}
209
210
	if err = service.repository.Delete(ctx, user); err != nil {
211
		msg := fmt.Sprintf("could not delete user with ID [%s] from the [%T]", userID, service.repository)
212
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
213
	}
214
215
	ctxLogger.Info(fmt.Sprintf("sucessfully deleted user with ID [%s] in the [%T]", userID, service.repository))
216
217
	event, err := service.createEvent(events.UserAccountDeleted, source, &events.UserAccountDeletedPayload{
218
		UserID:    userID,
219
		Timestamp: time.Now().UTC(),
220
	})
221
	if err != nil {
222
		msg := fmt.Sprintf("cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID)
223
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
224
	}
225
226
	if err = service.dispatcher.Dispatch(ctx, event); err != nil {
227
		msg := fmt.Sprintf("cannot dispatch [%s] event for user [%s]", event.Type(), userID)
228
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
229
	}
230
231
	return nil
232
}
233
234
// SendAPIKeyRotatedEmail sends an email to an entities.User when the API key is rotated
235
func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload *events.UserAPIKeyRotatedPayload) error {
236
	ctx, span := service.tracer.Start(ctx)
237
	defer span.End()
238
239
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
240
241
	email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone)
242
	if err != nil {
243
		msg := fmt.Sprintf("cannot create api key rotated email for user [%s]", payload.UserID)
244
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
245
	}
246
247
	if err = service.mailer.Send(ctx, email); err != nil {
248
		msg := fmt.Sprintf("canot create api key rotated email to user [%s]", payload.UserID)
249
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
250
	}
251
252
	ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID  [%s]", payload.Email, payload.UserID))
253
	return nil
254
}
255
256
// UserSendPhoneDeadEmailParams are parameters for notifying a user when a phone is dead
257
type UserSendPhoneDeadEmailParams struct {
258
	UserID                 entities.UserID
259
	PhoneID                uuid.UUID
260
	Owner                  string
261
	LastHeartbeatTimestamp time.Time
262
}
263
264
// SendPhoneDeadEmail sends an email to an entities.User when a phone is dead
265
func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *UserSendPhoneDeadEmailParams) error {
266
	ctx, span := service.tracer.Start(ctx)
267
	defer span.End()
268
269
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
270
271
	user, err := service.repository.Load(ctx, params.UserID)
272
	if err != nil {
273
		msg := fmt.Sprintf("could not get [%T] with ID [%s]", user, params.UserID)
274
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
275
	}
276
277
	if !user.NotificationHeartbeatEnabled {
278
		ctxLogger.Info(fmt.Sprintf("[%s] email notifications disabled for user [%s] with owner [%s]", events.EventTypePhoneHeartbeatOffline, params.UserID, params.Owner))
279
		return nil
280
	}
281
282
	email, err := service.emailFactory.PhoneDead(user, params.LastHeartbeatTimestamp, params.Owner)
283
	if err != nil {
284
		msg := fmt.Sprintf("cannot create phone dead email for user [%s]", params.UserID)
285
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
286
	}
287
288
	if err = service.mailer.Send(ctx, email); err != nil {
289
		msg := fmt.Sprintf("canot send phone dead notification to user [%s]", params.UserID)
290
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
291
	}
292
293
	ctxLogger.Info(fmt.Sprintf("phone dead notification sent successfully to [%s] about [%s]", user.Email, params.Owner))
294
	return nil
295
}
296
297
// StartSubscription starts a subscription for an entities.User
298
func (service *UserService) StartSubscription(ctx context.Context, params *events.UserSubscriptionCreatedPayload) error {
299
	ctx, span := service.tracer.Start(ctx)
300
	defer span.End()
301
302
	user, err := service.repository.Load(ctx, params.UserID)
303
	if err != nil {
304
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
305
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
306
	}
307
308
	user.SubscriptionID = &params.SubscriptionID
309
	user.SubscriptionName = params.SubscriptionName
310
	user.SubscriptionRenewsAt = &params.SubscriptionRenewsAt
311
	user.SubscriptionStatus = &params.SubscriptionStatus
312
	user.SubscriptionEndsAt = nil
313
314
	if err = service.repository.Update(ctx, user); err != nil {
315
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after update", user, params.UserID)
316
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
317
	}
318
319
	return nil
320
}
321
322
// InitiateSubscriptionCancel initiates the cancelling of a subscription on lemonsqueezy
323
func (service *UserService) InitiateSubscriptionCancel(ctx context.Context, userID entities.UserID) error {
324
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
325
	defer span.End()
326
327
	user, err := service.repository.Load(ctx, userID)
328
	if err != nil {
329
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, userID)
330
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
331
	}
332
333
	if _, _, err = service.lemonsqueezyClient.Subscriptions.Cancel(ctx, *user.SubscriptionID); err != nil {
334
		msg := fmt.Sprintf("could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)
335
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
336
	}
337
338
	ctxLogger.Info(fmt.Sprintf("cancelled subscription [%s] for user [%s]", *user.SubscriptionID, user.ID))
339
	return nil
340
}
341
342
// GetSubscriptionUpdateURL initiates the cancelling of a subscription on lemonsqueezy
343
func (service *UserService) GetSubscriptionUpdateURL(ctx context.Context, userID entities.UserID) (url string, err error) {
344
	ctx, span := service.tracer.Start(ctx)
345
	defer span.End()
346
347
	user, err := service.repository.Load(ctx, userID)
348
	if err != nil {
349
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, userID)
350
		return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
351
	}
352
353
	subscription, _, err := service.lemonsqueezyClient.Subscriptions.Get(ctx, *user.SubscriptionID)
354
	if err != nil {
355
		msg := fmt.Sprintf("could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)
356
		return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
357
	}
358
359
	return subscription.Data.Attributes.Urls.CustomerPortal, nil
360
}
361
362
// CancelSubscription starts a subscription for an entities.User
363
func (service *UserService) CancelSubscription(ctx context.Context, params *events.UserSubscriptionCancelledPayload) error {
364
	ctx, span := service.tracer.Start(ctx)
365
	defer span.End()
366
367
	user, err := service.repository.Load(ctx, params.UserID)
368
	if err != nil {
369
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
370
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
371
	}
372
373
	user.SubscriptionID = &params.SubscriptionID
374
	user.SubscriptionName = params.SubscriptionName
375
	user.SubscriptionRenewsAt = nil
376
	user.SubscriptionStatus = &params.SubscriptionStatus
377
	user.SubscriptionEndsAt = &params.SubscriptionEndsAt
378
379
	if err = service.repository.Update(ctx, user); err != nil {
380
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after update", user, params.UserID)
381
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
382
	}
383
384
	return nil
385
}
386
387
// ExpireSubscription finishes a subscription for an entities.User
388
func (service *UserService) ExpireSubscription(ctx context.Context, params *events.UserSubscriptionExpiredPayload) error {
389
	ctx, span := service.tracer.Start(ctx)
390
	defer span.End()
391
392
	user, err := service.repository.Load(ctx, params.UserID)
393
	if err != nil {
394
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
395
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
396
	}
397
398
	user.SubscriptionID = nil
399
	user.SubscriptionName = entities.SubscriptionNameFree
400
	user.SubscriptionRenewsAt = nil
401
	user.SubscriptionStatus = nil
402
	user.SubscriptionEndsAt = nil
403
404
	if err = service.repository.Update(ctx, user); err != nil {
405
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID)
406
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
407
	}
408
409
	return nil
410
}
411
412
// UpdateSubscription updates a subscription for an entities.User
413
func (service *UserService) UpdateSubscription(ctx context.Context, params *events.UserSubscriptionUpdatedPayload) error {
414
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
415
	defer span.End()
416
417
	user, err := service.repository.Load(ctx, params.UserID)
418
	if err != nil {
419
		msg := fmt.Sprintf("could not get [%T] with with ID [%s]", user, params.UserID)
420
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
421
	}
422
423
	if params.SubscriptionStatus != "active" {
424
		msg := fmt.Sprintf("subscription status is [%s] for [%T] with with ID [%s]", params.SubscriptionStatus, user, params.UserID)
425
		ctxLogger.Info(msg)
426
		return nil
427
	}
428
429
	user.SubscriptionID = &params.SubscriptionID
430
	user.SubscriptionName = params.SubscriptionName
431
	user.SubscriptionEndsAt = params.SubscriptionEndsAt
432
	user.SubscriptionRenewsAt = &params.SubscriptionRenewsAt
433
	user.SubscriptionStatus = &params.SubscriptionStatus
434
435
	if err = service.repository.Update(ctx, user); err != nil {
436
		msg := fmt.Sprintf("could not update [%T] with with ID [%s] after subscription update", user, params.UserID)
437
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
438
	}
439
440
	return nil
441
}
442
443
// DeleteAuthUser deletes an entities.AuthUser from firebase
444
func (service *UserService) DeleteAuthUser(ctx context.Context, userID entities.UserID) error {
445
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
446
	defer span.End()
447
448
	if err := service.authClient.DeleteUser(ctx, userID.String()); err != nil {
449
		msg := fmt.Sprintf("could not delete [entities.AuthUser] from firebase with ID [%s]", userID)
450
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
451
	}
452
453
	ctxLogger.Info(fmt.Sprintf("deleted [entities.AuthUser] from firebase for user with ID [%s]", userID))
454
	return nil
455
}
456