services.*UserService.Delete   B
last analyzed

Complexity

Conditions 8

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 24
dl 0
loc 37
rs 7.3333
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
	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