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

services.*UserService.dispatchUserCreatedEvent   A

Complexity

Conditions 3

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
dl 0
loc 18
rs 9.7
c 0
b 0
f 0
nop 3
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