• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vocdoni / saas-backend / 21756255886

06 Feb 2026 03:35PM UTC coverage: 63.488% (+0.04%) from 63.45%
21756255886

Pull #416

github

emmdim
feat(subscriptions): limit memberbase size based on census size
Pull Request #416: feat(subscriptions): limit memberbase size based on census size

29 of 39 new or added lines in 3 files covered. (74.36%)

45 existing lines in 1 file now uncovered.

7183 of 11314 relevant lines covered (63.49%)

40.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.73
/subscriptions/subscriptions.go
1
// Package subscriptions provides functionality for managing organization subscriptions
2
// and enforcing permissions based on subscription plans.
3
package subscriptions
4

5
import (
6
        "fmt"
7

8
        "github.com/ethereum/go-ethereum/common"
9
        "github.com/vocdoni/saas-backend/db"
10
        "github.com/vocdoni/saas-backend/errors"
11
        "go.vocdoni.io/proto/build/go/models"
12
)
13

14
// Config holds the configuration for the subscriptions service.
15
// It includes a reference to the MongoDB storage used by the service.
16
type Config struct {
17
        DB *db.MongoStorage
18
}
19

20
// DBPermission represents the permissions that an organization can have based on its subscription.
21
type DBPermission int
22

23
const (
24
        // InviteUser represents the permission to invite new users to an organization.
25
        InviteUser DBPermission = iota
26
        // DeleteUser represents the permission to remove users from an organization.
27
        DeleteUser
28
        // CreateSubOrg represents the permission to create sub-organizations.
29
        CreateSubOrg
30
        // CreateDraft represents the permission to create draft processes.
31
        CreateDraft
32
        // CreateOrg represents the permission to create new organizations.
33
        CreateOrg
34
)
35

36
const MaxOrgsPerUser = 15
37

38
// String returns the string representation of the DBPermission.
39
func (p DBPermission) String() string {
×
40
        switch p {
×
41
        case InviteUser:
×
42
                return "InviteUser"
×
43
        case DeleteUser:
×
44
                return "DeleteUser"
×
45
        case CreateSubOrg:
×
46
                return "CreateSubOrg"
×
47
        case CreateDraft:
×
48
                return "CreateDraft"
×
49
        case CreateOrg:
×
50
                return "CreateOrg"
×
51
        default:
×
52
                return "Unknown"
×
53
        }
54
}
55

56
// DBInterface defines the database methods required by the Subscriptions service
57
type DBInterface interface {
58
        Plan(id uint64) (*db.Plan, error)
59
        UserByEmail(email string) (*db.User, error)
60
        Organization(address common.Address) (*db.Organization, error)
61
        OrganizationWithParent(address common.Address) (*db.Organization, *db.Organization, error)
62
        CountOrgMembers(orgAddress common.Address) (int64, error)
63
        CountProcesses(orgAddress common.Address, draft db.DraftFilter) (int64, error)
64
        OrganizationMemberGroup(groupID string, orgAddress common.Address) (*db.OrganizationMemberGroup, error)
65
}
66

67
// Subscriptions is the service that manages the organization permissions based on
68
// the subscription plans.
69
type Subscriptions struct {
70
        db DBInterface
71
}
72

73
// New creates a new Subscriptions service with the given configuration.
74
func New(conf *Config) *Subscriptions {
1✔
75
        if conf == nil {
1✔
76
                return nil
×
77
        }
×
78
        return &Subscriptions{
1✔
79
                db: conf.DB,
1✔
80
        }
1✔
81
}
82

83
// hasElectionMetadataPermissions checks if the organization has permission to create an election with the given metadata.
84
func hasElectionMetadataPermissions(process *models.NewProcessTx, plan *db.Plan) (bool, error) {
3✔
85
        // check ANONYMOUS
3✔
86
        if process.Process.EnvelopeType.Anonymous && !plan.Features.Anonymous {
3✔
87
                return false, fmt.Errorf("anonymous elections are not allowed")
×
88
        }
×
89

90
        // check WEIGHTED
91
        if process.Process.EnvelopeType.CostFromWeight && !plan.VotingTypes.Weighted {
3✔
92
                return false, fmt.Errorf("weighted elections are not allowed")
×
93
        }
×
94

95
        // check VOTE OVERWRITE
96
        if process.Process.VoteOptions.MaxVoteOverwrites > 0 && !plan.Features.Overwrite {
3✔
97
                return false, fmt.Errorf("vote overwrites are not allowed")
×
98
        }
×
99

100
        // check PROCESS DURATION
101
        duration := plan.Organization.MaxDuration * 24 * 60 * 60
3✔
102
        if process.Process.Duration > uint32(duration) {
3✔
103
                return false, fmt.Errorf("duration is greater than the allowed")
×
104
        }
×
105

106
        // TODO:future check if the election voting type is supported by the plan
107
        // TODO:future check if the streamURL is used and allowed by the plan
108

109
        return true, nil
3✔
110
}
111

112
// HasTxPermission checks if the organization has permission to perform the given transaction.
113
func (p *Subscriptions) HasTxPermission(
114
        tx *models.Tx,
115
        txType models.TxType,
116
        org *db.Organization,
117
        user *db.User,
118
) (bool, error) {
8✔
119
        if org == nil {
9✔
120
                return false, errors.ErrInvalidData.With("organization is nil")
1✔
121
        }
1✔
122

123
        // Check if the organization has a subscription
124
        if org.Subscription.PlanID == 0 {
8✔
125
                return false, errors.ErrOrganizationHasNoSubscription
1✔
126
        }
1✔
127

128
        plan, err := p.db.Plan(org.Subscription.PlanID)
6✔
129
        if err != nil {
6✔
130
                return false, errors.ErrPlanNotFound.WithErr(err)
×
131
        }
×
132

133
        switch txType {
6✔
134
        // check UPDATE ACCOUNT INFO
135
        case models.TxType_SET_ACCOUNT_INFO_URI:
1✔
136
                // check if the user has the admin role for the organization
1✔
137
                if !user.HasRoleFor(org.Address, db.AdminRole) {
1✔
138
                        return false, errors.ErrUserHasNoAdminRole
×
139
                }
×
140
        // check CREATE PROCESS
141
        case models.TxType_NEW_PROCESS, models.TxType_SET_PROCESS_CENSUS:
3✔
142
                // check if the user has the admin role for the organization
3✔
143
                if !user.HasRoleFor(org.Address, db.AdminRole) {
3✔
144
                        return false, errors.ErrUserHasNoAdminRole
×
145
                }
×
146
                newProcess := tx.GetNewProcess()
3✔
147
                if newProcess.Process.MaxCensusSize > uint64(plan.Organization.MaxCensus) {
3✔
148
                        return false, errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("plan max census: %d", plan.Organization.MaxCensus)
×
149
                }
×
150
                if org.Counters.Processes >= plan.Organization.MaxProcesses {
3✔
151
                        // allow processes with less than TestMaxCensusSize for user testing
×
152
                        if newProcess.Process.MaxCensusSize > uint64(db.TestMaxCensusSize) {
×
153
                                return false, errors.ErrMaxProcessesReached
×
154
                        }
×
155
                }
156
                return hasElectionMetadataPermissions(newProcess, plan)
3✔
157

158
        case models.TxType_SET_PROCESS_STATUS,
159
                models.TxType_CREATE_ACCOUNT:
2✔
160
                // check if the user has the admin role for the organization
2✔
161
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
2✔
162
                        return false, errors.ErrUserHasNoAdminRole
×
163
                }
×
164
        default:
×
165
                return false, fmt.Errorf("unsupported txtype")
×
166
        }
167
        return true, nil
3✔
168
}
169

170
// HasDBPermission checks if the user has permission to perform the given action in the organization stored in the DB
171
func (p *Subscriptions) HasDBPermission(userEmail string, orgAddress common.Address, permission DBPermission) (bool, error) {
87✔
172
        user, err := p.db.UserByEmail(userEmail)
87✔
173
        if err != nil {
88✔
174
                return false, fmt.Errorf("could not get user: %v", err)
1✔
175
        }
1✔
176
        switch permission {
86✔
177
        case InviteUser, DeleteUser, CreateSubOrg:
30✔
178
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
34✔
179
                        return false, errors.ErrUserHasNoAdminRole
4✔
180
                }
4✔
181
                return true, nil
26✔
182
        case CreateOrg:
56✔
183
                // Check if the user can create more organizations based on the MaxOrgsPerUser limit
56✔
184
                if len(user.Organizations) >= MaxOrgsPerUser {
56✔
185
                        return false, errors.ErrMaxOrganizationsReached.Withf(
×
186
                                "user is part of %d organizations, max allowed is %d",
×
187
                                len(user.Organizations),
×
188
                                MaxOrgsPerUser,
×
189
                        )
×
190
                }
×
191
                return true, nil
56✔
192
        default:
×
193
                return false, fmt.Errorf("permission not found")
×
194
        }
195
}
196

197
// OrgHasPermission checks if the org has permission to perform the given action
198
func (p *Subscriptions) OrgHasPermission(orgAddress common.Address, permission DBPermission) error {
8✔
199
        switch permission {
8✔
200
        case CreateDraft:
8✔
201
                // Check if the organization has a subscription
8✔
202
                org, err := p.db.Organization(orgAddress)
8✔
203
                if err != nil {
8✔
204
                        return errors.ErrOrganizationNotFound.WithErr(err)
×
205
                }
×
206

207
                if org.Subscription.PlanID == 0 {
8✔
208
                        return errors.ErrOrganizationHasNoSubscription.With("can't create draft process")
×
209
                }
×
210

211
                plan, err := p.db.Plan(org.Subscription.PlanID)
8✔
212
                if err != nil {
8✔
213
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
214
                }
×
215

216
                count, err := p.db.CountProcesses(orgAddress, db.DraftOnly)
8✔
217
                if err != nil {
8✔
218
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
219
                }
×
220

221
                if count >= int64(plan.Organization.MaxDrafts) {
10✔
222
                        return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
2✔
223
                }
2✔
224
                return nil
6✔
225
        default:
×
226
                return fmt.Errorf("permission not found")
×
227
        }
228
}
229

230
func (p *Subscriptions) OrgCanAddNMembers(orgAddress common.Address, memberNumber int) error {
29✔
231
        // Check if the organization has a subscription
29✔
232
        org, err := p.db.Organization(orgAddress)
29✔
233
        if err != nil {
29✔
NEW
234
                return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
235
        }
×
236

237
        if org.Subscription.PlanID == 0 {
29✔
NEW
238
                return errors.ErrOrganizationHasNoSubscription.With("can't create draft process")
×
NEW
239
        }
×
240

241
        plan, err := p.db.Plan(org.Subscription.PlanID)
29✔
242
        if err != nil {
29✔
NEW
243
                return errors.ErrGenericInternalServerError.WithErr(err)
×
NEW
244
        }
×
245

246
        count, err := p.db.CountOrgMembers(orgAddress)
29✔
247
        if err != nil {
29✔
NEW
248
                return errors.ErrGenericInternalServerError.WithErr(err)
×
NEW
249
        }
×
250

251
        if int(count)+memberNumber > plan.Organization.MaxCensus {
31✔
252
                return errors.ErrExceedsOrganizationMembersLimit.Withf("(%d)", plan.Organization.MaxCensus)
2✔
253
        }
2✔
254
        return nil
27✔
255
}
256

257
func (p *Subscriptions) OrgCanPublishGroupCensus(census *db.Census, groupID string) error {
26✔
258
        org, err := p.db.Organization(census.OrgAddress)
26✔
259
        if err != nil {
26✔
260
                return errors.ErrOrganizationNotFound.WithErr(err)
×
261
        }
×
262

263
        if org.Subscription.PlanID == 0 {
26✔
264
                return errors.ErrOrganizationHasNoSubscription
×
265
        }
×
266

267
        plan, err := p.db.Plan(org.Subscription.PlanID)
26✔
268
        if err != nil {
26✔
269
                return errors.ErrPlanNotFound.WithErr(err)
×
270
        }
×
271

272
        group, err := p.db.OrganizationMemberGroup(groupID, org.Address)
26✔
273
        if err != nil {
26✔
274
                return errors.ErrGroupNotFound.WithErr(err)
×
275
        }
×
276

277
        remainingEmails := plan.Features.TwoFaEmail - org.Counters.SentEmails
26✔
278
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldEmail) && len(group.MemberIDs) > remainingEmails {
27✔
279
                return errors.ErrProcessCensusSizeExceedsEmailAllowance.Withf("remaining emails: %d", remainingEmails)
1✔
280
        }
1✔
281
        remainingSMS := plan.Features.TwoFaSms - org.Counters.SentSMS
25✔
282
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldPhone) && len(group.MemberIDs) > remainingSMS {
28✔
283
                return errors.ErrProcessCensusSizeExceedsSMSAllowance.Withf("remaining sms: %d", remainingSMS)
3✔
284
        }
3✔
285

286
        return nil
22✔
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc