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

services.*UserService.Delete   B

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.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