forked from NdoleStudio/httpsms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathentitlement_service.go
More file actions
149 lines (131 loc) · 4.2 KB
/
Copy pathentitlement_service.go
File metadata and controls
149 lines (131 loc) · 4.2 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
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"
pluralize "github.com/gertd/go-pluralize"
"github.com/palantir/stacktrace"
)
// 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) {
ctx, span := service.tracer.Start(ctx)
defer span.End()
if !service.enabled {
return &EntitlementCheckResult{Allowed: true}, nil
}
limits, exists := entityLimits[entityName]
if !exists {
return &EntitlementCheckResult{Allowed: true}, nil
}
user, err := service.userRepository.Load(ctx, userID)
if err != nil {
return nil, service.tracer.WrapErrorSpan(
span,
stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s] for entitlement check", userID)),
)
}
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.Propagate(err, fmt.Sprintf("cannot count entities [%s] for user [%s]", entityName, userID)),
)
}
if currentCount >= limit {
return &EntitlementCheckResult{
Allowed: false,
Message: fmt.Sprintf(
"Upgrade to a paid 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, " ")
}