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

api/pkg/services/billing_service.go   B

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 43
eloc 151
dl 0
loc 240
rs 8.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A services.NewBillingService 0 17 1
A services.*BillingService.handleLimitExceeded 0 12 1
A services.*BillingService.IsEntitled 0 2 1
A services.*BillingService.GetCurrentUsage 0 5 1
A services.*BillingService.IsEntitledWithCount 0 23 4
A services.*BillingService.RegisterReceivedMessage 0 14 2
A services.*BillingService.RegisterSentMessage 0 14 2
A services.*BillingService.GetUsageHistory 0 5 1
A services.*BillingService.sendLimitExceededEmail 0 24 5
A services.*BillingService.DeleteAllForUser 0 11 2
B services.*BillingService.sendUsageAlert 0 34 6
F services.*BillingService.shouldSendAlert 0 18 17
1
package services
2
3
import (
4
	"context"
5
	"fmt"
6
	"time"
7
8
	"github.com/NdoleStudio/httpsms/pkg/cache"
9
	"github.com/NdoleStudio/httpsms/pkg/emails"
10
11
	"github.com/NdoleStudio/httpsms/pkg/entities"
12
	"github.com/NdoleStudio/httpsms/pkg/repositories"
13
	"github.com/NdoleStudio/httpsms/pkg/telemetry"
14
	"github.com/google/uuid"
15
	"github.com/palantir/stacktrace"
16
)
17
18
// BillingService is responsible for tracking usages and billing users
19
type BillingService struct {
20
	service
21
	logger                 telemetry.Logger
22
	tracer                 telemetry.Tracer
23
	cache                  cache.Cache
24
	emailFactory           emails.UserEmailFactory
25
	mailer                 emails.Mailer
26
	userRepository         repositories.UserRepository
27
	billingUsageRepository repositories.BillingUsageRepository
28
}
29
30
// NewBillingService creates a new BillingService
31
func NewBillingService(
32
	logger telemetry.Logger,
33
	tracer telemetry.Tracer,
34
	cache cache.Cache,
35
	mailer emails.Mailer,
36
	emailFactory emails.UserEmailFactory,
37
	usageRepository repositories.BillingUsageRepository,
38
	userRepository repositories.UserRepository,
39
) (s *BillingService) {
40
	return &BillingService{
41
		logger:                 logger.WithService(fmt.Sprintf("%T", s)),
42
		tracer:                 tracer,
43
		cache:                  cache,
44
		emailFactory:           emailFactory,
45
		mailer:                 mailer,
46
		userRepository:         userRepository,
47
		billingUsageRepository: usageRepository,
48
	}
49
}
50
51
// IsEntitledWithCount checks if a user can send or receive and SMS message
52
func (service *BillingService) IsEntitledWithCount(ctx context.Context, userID entities.UserID, count uint) *string {
53
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
54
	defer span.End()
55
56
	user, err := service.userRepository.Load(ctx, userID)
57
	if err != nil {
58
		msg := fmt.Sprintf("cannot load user with ID [%s], entitlement successfull", userID)
59
		ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
60
		return nil
61
	}
62
63
	usage, err := service.billingUsageRepository.GetCurrent(ctx, userID)
64
	if err != nil {
65
		msg := fmt.Sprintf("cannot load billing usage for user with ID [%s], entitlement successfull", userID)
66
		ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
67
		return nil
68
	}
69
70
	if !usage.IsEntitled(count, user.SubscriptionName.Limit()) {
71
		return service.handleLimitExceeded(ctx, user)
72
	}
73
74
	return nil
75
}
76
77
// IsEntitled checks if a user can send or receive and SMS message
78
func (service *BillingService) IsEntitled(ctx context.Context, userID entities.UserID) *string {
79
	return service.IsEntitledWithCount(ctx, userID, 1)
80
}
81
82
func (service *BillingService) handleLimitExceeded(ctx context.Context, user *entities.User) *string {
83
	ctx, span := service.tracer.Start(ctx)
84
	defer span.End()
85
86
	service.sendLimitExceededEmail(ctx, user)
87
88
	message := fmt.Sprintf(
89
		"You have exceeded your limit of [%d] messages on your [%s] plan. Upgrade to send more messages on https://httpsms.com/billing",
90
		user.SubscriptionName.Limit(),
91
		user.SubscriptionName,
92
	)
93
	return &message
94
}
95
96
func (service *BillingService) sendLimitExceededEmail(ctx context.Context, user *entities.User) {
97
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
98
	defer span.End()
99
100
	key := fmt.Sprintf("user.limit.exceeded.%s", user.ID)
101
	if _, err := service.cache.Get(ctx, key); err == nil {
102
		return
103
	}
104
105
	email, err := service.emailFactory.UsageLimitExceeded(user)
106
	if err != nil {
107
		ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot create usage limit email for user [%s]", user.ID)))
108
		return
109
	}
110
111
	if err = service.mailer.Send(ctx, email); err != nil {
112
		msg := fmt.Sprintf("canot send usage limit exceeded notification to user [%s]", user.ID)
113
		ctxLogger.Error(stacktrace.Propagate(err, msg))
114
		return
115
	}
116
117
	ctxLogger.Info(fmt.Sprintf("usage limit exceeded email sent to user [%s]", user.ID))
118
	if err = service.cache.Set(ctx, key, "", time.Hour*12); err != nil {
119
		ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot set item in redis with key [%s]", key)))
120
	}
121
}
122
123
// GetCurrentUsage gets the current billing usage for a user
124
func (service *BillingService) GetCurrentUsage(ctx context.Context, userID entities.UserID) (*entities.BillingUsage, error) {
125
	ctx, span := service.tracer.Start(ctx)
126
	defer span.End()
127
128
	return service.billingUsageRepository.GetCurrent(ctx, userID)
129
}
130
131
// GetUsageHistory gets the billing usage history for a user
132
func (service *BillingService) GetUsageHistory(ctx context.Context, userID entities.UserID, params repositories.IndexParams) (*[]entities.BillingUsage, error) {
133
	ctx, span := service.tracer.Start(ctx)
134
	defer span.End()
135
136
	return service.billingUsageRepository.GetHistory(ctx, userID, params)
137
}
138
139
// RegisterSentMessage records the billing usage for a sent message
140
func (service *BillingService) RegisterSentMessage(ctx context.Context, messageID uuid.UUID, timestamp time.Time, userID entities.UserID) error {
141
	ctx, span := service.tracer.Start(ctx)
142
	defer span.End()
143
144
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
145
146
	if err := service.billingUsageRepository.RegisterSentMessage(ctx, timestamp, userID); err != nil {
147
		msg := fmt.Sprintf("could not register [sent] message with ID [%s] for user with ID [%s]", messageID, userID)
148
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
149
	}
150
151
	ctxLogger.Info(fmt.Sprintf("registered [sent] message with ID [%s] for user [%s]", messageID, userID))
152
	service.sendUsageAlert(ctx, userID)
153
	return nil
154
}
155
156
// RegisterReceivedMessage records the billing usage for a received message
157
func (service *BillingService) RegisterReceivedMessage(ctx context.Context, messageID uuid.UUID, timestamp time.Time, userID entities.UserID) error {
158
	ctx, span := service.tracer.Start(ctx)
159
	defer span.End()
160
161
	ctxLogger := service.tracer.CtxLogger(service.logger, span)
162
163
	if err := service.billingUsageRepository.RegisterReceivedMessage(ctx, timestamp, userID); err != nil {
164
		msg := fmt.Sprintf("could not register [received] message with ID [%s] for user with ID [%s]", messageID, userID)
165
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
166
	}
167
168
	ctxLogger.Info(fmt.Sprintf("registered [received] message with ID [%s] for user [%s]", messageID, userID))
169
	service.sendUsageAlert(ctx, userID)
170
	return nil
171
}
172
173
// DeleteAllForUser deletes all entities.BillingUsage for an entities.UserID.
174
func (service *BillingService) DeleteAllForUser(ctx context.Context, userID entities.UserID) error {
175
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
176
	defer span.End()
177
178
	if err := service.billingUsageRepository.DeleteAllForUser(ctx, userID); err != nil {
179
		msg := fmt.Sprintf("could not delete [entities.BillingUsage] for user with ID [%s]", userID)
180
		return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
181
	}
182
183
	ctxLogger.Info(fmt.Sprintf("deleted all [entities.BillingUsage] for user with ID [%s]", userID))
184
	return nil
185
}
186
187
func (service *BillingService) sendUsageAlert(ctx context.Context, userID entities.UserID) {
188
	ctx, span, ctxLogger := service.tracer.StartWithLogger(ctx, service.logger)
189
	defer span.End()
190
191
	user, err := service.userRepository.Load(ctx, userID)
192
	if err != nil {
193
		msg := fmt.Sprintf("cannot load user with ID [%s]", userID)
194
		ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
195
		return
196
	}
197
198
	billingUsage, err := service.billingUsageRepository.GetCurrent(ctx, userID)
199
	if err != nil {
200
		msg := fmt.Sprintf("cannot load billing usage for user with ID [%s]", userID)
201
		ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
202
		return
203
	}
204
205
	if !service.shouldSendAlert(user, billingUsage) {
206
		return
207
	}
208
209
	email, err := service.emailFactory.UsageLimitAlert(user, billingUsage)
210
	if err != nil {
211
		ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot create usage alert email for user [%s]", user.ID)))
212
		return
213
	}
214
215
	if err = service.mailer.Send(ctx, email); err != nil {
216
		msg := fmt.Sprintf("canot send usage alert notification to user [%s]", user.ID)
217
		ctxLogger.Error(stacktrace.Propagate(err, msg))
218
	}
219
220
	ctxLogger.Info(fmt.Sprintf("usage alert email sent to user [%s]", user.ID))
221
}
222
223
func (service *BillingService) shouldSendAlert(user *entities.User, usage *entities.BillingUsage) bool {
224
	if user.IsOnFreePlan() && (usage.TotalMessages() == 160 || usage.TotalMessages() == 180 || usage.TotalMessages() == 190) {
225
		return true
226
	}
227
228
	if user.IsOnProPlan() && (usage.TotalMessages() == 4000 || usage.TotalMessages() == 4500 || usage.TotalMessages() == 4750) {
229
		return true
230
	}
231
232
	if user.IsOnUltraPlan() && (usage.TotalMessages() == 8000 || usage.TotalMessages() == 9000 || usage.TotalMessages() == 9500) {
233
		return true
234
	}
235
236
	if user.IsOn20kPlan() && (usage.TotalMessages() == 16000 || usage.TotalMessages() == 18000 || usage.TotalMessages() == 19000) {
237
		return true
238
	}
239
240
	return false
241
}
242