Repository navigation
Expand file tree
/
Copy pathentitlement_service.go
More file actions
165 lines (146 loc) · 4.79 KB
/
Copy pathentitlement_service.go
File metadata and controls
165 lines (146 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package services
import (
"context"
"fmt"
"strings"
"unicode"
"github.com/NdoleStudio/httpsms/pkg/entities"
"github.com/NdoleStudio/httpsms/pkg/repositories"
"github.com/NdoleStudio/httpsms/pkg/telemetry"
"github.com/NdoleStudio/stacktrace"
pluralize "github.com/gertd/go-pluralize"
)
// entityLimits maps entity name → subscription plan → max count.
// A limit of 0 means unlimited. If a plan is not listed, it defaults to unlimited (0).
var entityLimits = map[string]map[entities.SubscriptionName]int{
entities.EntityNameMessageSendSchedule: {
entities.SubscriptionNameFree: 1,
},
entities.EntityNamePhoneAPIKey: {
entities.SubscriptionNameFree: 1,
},
}
// EntitlementCheckResult holds the outcome of an entitlement check.
type EntitlementCheckResult struct {
Allowed bool
Message string
}
// EntitlementService checks whether a user can create more of a given entity
// based on their subscription plan.
type EntitlementService struct {
service
logger telemetry.Logger
tracer telemetry.Tracer
enabled bool
userRepository repositories.UserRepository
}
// NewEntitlementService creates a new EntitlementService.
// The enabled flag should come from the ENTITLEMENT_ENABLED environment variable.
func NewEntitlementService(
logger telemetry.Logger,
tracer telemetry.Tracer,
enabled bool,
userRepository repositories.UserRepository,
) *EntitlementService {
return &EntitlementService{
logger: logger.WithService(fmt.Sprintf("%T", &EntitlementService{})),
tracer: tracer,
enabled: enabled,
userRepository: userRepository,
}
}
// Check verifies if the user can create another instance of the given entity.
func (service *EntitlementService) Check(
ctx context.Context,
userID entities.UserID,
entityName string,
countFunc func() (int, error),
) (*EntitlementCheckResult, error) {
return service.CheckAdditional(ctx, userID, entityName, 1, countFunc)
}
// CheckAdditional verifies if the user can create additionalCount instances of
// the given entity without exceeding their subscription plan limit.
func (service *EntitlementService) CheckAdditional(
ctx context.Context,
userID entities.UserID,
entityName string,
additionalCount int,
countFunc func() (int, error),
) (*EntitlementCheckResult, error) {
ctx, span := service.tracer.Start(ctx)
defer span.End()
if !service.enabled {
return &EntitlementCheckResult{Allowed: true}, nil
}
limits, hasConfiguredLimits := entityLimits[entityName]
if !hasConfiguredLimits && entityName != entities.EntityNameContact {
return &EntitlementCheckResult{Allowed: true}, nil
}
user, err := service.userRepository.Load(ctx, userID)
if err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
stacktrace.Propagatef(err, "cannot load user [%s] for entitlement check", userID),
)
}
limit := int(user.SubscriptionName.Limit())
if entityName != entities.EntityNameContact {
var hasLimit bool
limit, hasLimit = limits[user.SubscriptionName]
if !hasLimit || limit == 0 {
return &EntitlementCheckResult{Allowed: true}, nil
}
}
currentCount, err := countFunc()
if err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
stacktrace.Propagatef(err, "cannot count entities [%s] for user [%s]", entityName, userID),
)
}
if currentCount+additionalCount > limit {
return &EntitlementCheckResult{
Allowed: false,
Message: fmt.Sprintf(
"Upgrade your plan to create more than [%d] %s. Visit https://httpsms.com/pricing for details.",
limit,
formatEntityName(entityName, true),
),
}, nil
}
return &EntitlementCheckResult{Allowed: true}, nil
}
// formatEntityName converts a PascalCase entity name to lowercase words and optionally pluralizes it.
// Consecutive uppercase letters (acronyms like API) are kept together as a single word.
// e.g. "MessageSendSchedule" → "message send schedules", "PhoneAPIKey" → "phone API keys"
func formatEntityName(name string, plural bool) string {
var words []string
runes := []rune(name)
start := 0
for i := 1; i < len(runes); i++ {
if unicode.IsUpper(runes[i]) {
if !unicode.IsUpper(runes[i-1]) {
// transition from lowercase to uppercase: split before i
words = append(words, string(runes[start:i]))
start = i
} else if i+1 < len(runes) && unicode.IsLower(runes[i+1]) {
// transition from uppercase run to a new word (e.g., "API" followed by "Key")
words = append(words, string(runes[start:i]))
start = i
}
}
}
words = append(words, string(runes[start:]))
for i, word := range words {
if word == strings.ToUpper(word) && len(word) > 1 {
// keep acronyms uppercase (e.g., "API")
continue
}
words[i] = strings.ToLower(word)
}
if plural && len(words) > 0 {
client := pluralize.NewClient()
words[len(words)-1] = client.Plural(words[len(words)-1])
}
return strings.Join(words, " ")
}