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

vocdoni / saas-backend / 29813735250

21 Jul 2026 08:17AM UTC coverage: 62.421%. First build
29813735250

Pull #583

github

web-flow
chore(lint): fix all golangci-lint issues for v2.12 (#584)
Pull Request #583: feat: stage upgrade

1584 of 2681 new or added lines in 47 files covered. (59.08%)

11795 of 18896 relevant lines covered (62.42%)

45.45 hits per line

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

66.32
/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
        stderrors "errors"
7
        "fmt"
8

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

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

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

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

37
const MaxOrgsPerUser = 15
38

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

57
// DBInterface defines the database methods required by the Subscriptions service
58
type DBInterface interface {
59
        Plan(id string) (*db.Plan, error)
60
        UserByEmail(email string) (*db.User, error)
61
        Organization(address common.Address) (*db.Organization, error)
62
        OrganizationWithParent(address common.Address) (*db.Organization, *db.Organization, error)
63
        CountCensusParticipants(censusID string) (int64, error)
64
        CountOrgMembers(orgAddress common.Address) (int64, error)
65
        CountMembersManagedBy(integratorAddr common.Address) (int64, error)
66
        SumSentEmailsManagedBy(integratorAddr common.Address) (int, error)
67
        SumSentSMSManagedBy(integratorAddr common.Address) (int, error)
68
        SumSentVotesManagedBy(integratorAddr common.Address) (int, error)
69
        CountProcesses(orgAddress common.Address, draft db.DraftFilter) (int64, error)
70
        CountVotingProcesses(orgAddress common.Address, draft db.DraftFilter) (int64, error)
71
        OrganizationMemberGroup(groupID string, orgAddress common.Address) (*db.OrganizationMemberGroup, error)
72
}
73

74
// Subscriptions is the service that manages the organization permissions based on
75
// the subscription plans.
76
type Subscriptions struct {
77
        db DBInterface
78
}
79

80
// New creates a new Subscriptions service with the given configuration.
81
func New(conf *Config) *Subscriptions {
1✔
82
        if conf == nil {
1✔
83
                return nil
×
84
        }
×
85
        return &Subscriptions{
1✔
86
                db: conf.DB,
1✔
87
        }
1✔
88
}
89

90
// hasElectionMetadataPermissions checks if the organization has permission to create an election with the given metadata.
91
func hasElectionMetadataPermissions(process *models.NewProcessTx, plan *db.Plan) (bool, error) {
28✔
92
        // check ANONYMOUS
28✔
93
        if process.Process.EnvelopeType.Anonymous && !plan.Features.Anonymous {
28✔
94
                return false, fmt.Errorf("anonymous elections are not allowed")
×
95
        }
×
96

97
        // check WEIGHTED
98
        if process.Process.EnvelopeType.CostFromWeight && !plan.VotingTypes.Weighted {
28✔
99
                return false, fmt.Errorf("weighted elections are not allowed")
×
100
        }
×
101

102
        // check VOTE OVERWRITE
103
        if process.Process.VoteOptions.MaxVoteOverwrites > 0 && !plan.Features.Overwrite {
28✔
104
                return false, fmt.Errorf("vote overwrites are not allowed")
×
105
        }
×
106

107
        // check PROCESS DURATION
108
        duration := plan.Organization.MaxDuration * 24 * 60 * 60
28✔
109
        if process.Process.Duration > uint32(duration) {
28✔
110
                return false, fmt.Errorf("duration is greater than the allowed")
×
111
        }
×
112

113
        // TODO:future check if the election voting type is supported by the plan
114
        // TODO:future check if the streamURL is used and allowed by the plan
115

116
        return true, nil
28✔
117
}
118

119
// managed reports whether org is a managed organization (created and owned by an integrator).
120
func managed(org *db.Organization) bool {
416✔
121
        return org != nil && org.ManagedBy != (common.Address{})
416✔
122
}
416✔
123

124
// limitsOwner returns the organization whose subscription plan governs usage limits for
125
// org, together with that plan. For a managed organization the owner is its integrator
126
// (org.ManagedBy); otherwise it is org itself. It fails closed if a managed org's
127
// integrator cannot be resolved or has no plan — there is no silent fallback to the
128
// managed org's own (throwaway default) plan.
129
func (p *Subscriptions) limitsOwner(org *db.Organization) (*db.Organization, *db.Plan, error) {
260✔
130
        if org == nil {
260✔
131
                return nil, nil, errors.ErrInvalidData.With("organization is nil")
×
132
        }
×
133
        owner := org
260✔
134
        if managed(org) {
281✔
135
                integrator, err := p.db.Organization(org.ManagedBy)
21✔
136
                if err != nil {
22✔
137
                        if stderrors.Is(err, db.ErrNotFound) {
2✔
138
                                return nil, nil, errors.ErrOrganizationNotFound.WithErr(err)
1✔
139
                        }
1✔
140
                        return nil, nil, errors.ErrGenericInternalServerError.WithErr(err)
×
141
                }
142
                owner = integrator
20✔
143
        }
144
        if owner.Subscription.PlanID == "" {
260✔
145
                return nil, nil, errors.ErrOrganizationHasNoSubscription
1✔
146
        }
1✔
147
        plan, err := p.db.Plan(owner.Subscription.PlanID)
258✔
148
        if err != nil {
258✔
149
                if stderrors.Is(err, db.ErrNotFound) {
×
150
                        return nil, nil, errors.ErrPlanNotFound.WithErr(err)
×
151
                }
×
152
                return nil, nil, errors.ErrGenericInternalServerError.WithErr(err)
×
153
        }
154
        return owner, plan, nil
258✔
155
}
156

157
// HasTxPermission checks if the organization has permission to perform the given transaction.
158
func (p *Subscriptions) HasTxPermission(
159
        tx *models.Tx,
160
        txType models.TxType,
161
        org *db.Organization,
162
        user *db.User,
163
) (bool, error) {
45✔
164
        if org == nil {
46✔
165
                return false, errors.ErrInvalidData.With("organization is nil")
1✔
166
        }
1✔
167

168
        // Resolve the plan that governs this org's limits: the integrator's plan for a
169
        // managed org, otherwise the org's own plan.
170
        _, plan, err := p.limitsOwner(org)
44✔
171
        if err != nil {
45✔
172
                return false, err
1✔
173
        }
1✔
174

175
        switch txType {
43✔
176
        // check UPDATE ACCOUNT INFO
177
        case models.TxType_SET_ACCOUNT_INFO_URI:
1✔
178
                // check if the user has the admin role for the organization
1✔
179
                if !user.HasRoleFor(org.Address, db.AdminRole) {
1✔
180
                        return false, errors.ErrUserHasNoAdminRole
×
181
                }
×
182
        // check CREATE PROCESS
183
        case models.TxType_NEW_PROCESS:
30✔
184
                // check if the user has the admin role for the organization
30✔
185
                if !user.HasRoleFor(org.Address, db.AdminRole) {
30✔
186
                        return false, errors.ErrUserHasNoAdminRole
×
187
                }
×
188
                newProcess := tx.GetNewProcess()
30✔
189
                if newProcess == nil || newProcess.Process == nil {
30✔
190
                        return false, errors.ErrInvalidData.With("missing new-process payload")
×
191
                }
×
192
                // A single process's declared census size is bounded by the governing plan's MaxCensus
193
                // for every org — the integrator's plan for a managed org, otherwise the org's own.
194
                if newProcess.Process.MaxCensusSize > uint64(plan.Organization.MaxCensus) {
32✔
195
                        return false, errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("plan max census: %d", plan.Organization.MaxCensus)
2✔
196
                }
2✔
197
                // The process-*count* limit, however, is enforced for managed orgs against the
198
                // integrator's aggregate quota (ReserveManagedPublish) at publish time, so the per-org
199
                // count check is skipped for them. Capability/duration checks below apply to all.
200
                if !managed(org) {
50✔
201
                        if org.Counters.Processes >= plan.Organization.MaxProcesses {
22✔
202
                                // allow processes with less than TestMaxCensusSize for user testing
×
203
                                if newProcess.Process.MaxCensusSize > uint64(db.TestMaxCensusSize) {
×
204
                                        return false, errors.ErrMaxProcessesReached
×
205
                                }
×
206
                        }
207
                }
208
                return hasElectionMetadataPermissions(newProcess, plan)
28✔
209

210
        // check UPDATE PROCESS CENSUS
211
        case models.TxType_SET_PROCESS_CENSUS:
2✔
212
                // check if the user has the admin role for the organization
2✔
213
                if !user.HasRoleFor(org.Address, db.AdminRole) {
2✔
214
                        return false, errors.ErrUserHasNoAdminRole
×
215
                }
×
216
                // A census update carries a SetProcess payload (not NewProcess), so it must be read with
217
                // GetSetProcess. Its new census size, when set, is bounded by the governing plan's
218
                // MaxCensus exactly like a new process — the integrator's plan for a managed org.
219
                setProcess := tx.GetSetProcess()
2✔
220
                if setProcess == nil {
2✔
221
                        return false, errors.ErrInvalidData.With("missing set-process payload")
×
222
                }
×
223
                if setProcess.GetCensusSize() > uint64(plan.Organization.MaxCensus) {
3✔
224
                        return false, errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("plan max census: %d", plan.Organization.MaxCensus)
1✔
225
                }
1✔
226

227
        case models.TxType_SET_PROCESS_STATUS,
228
                models.TxType_CREATE_ACCOUNT:
10✔
229
                // check if the user has the admin role for the organization
10✔
230
                if !user.HasRoleFor(org.Address, db.AdminRole) && !user.HasRoleFor(org.Address, db.ManagerRole) {
10✔
231
                        return false, errors.ErrUserHasNoAdminRole
×
232
                }
×
233
        default:
×
234
                return false, fmt.Errorf("unsupported txtype")
×
235
        }
236
        return true, nil
12✔
237
}
238

239
// HasDBPermission checks if the user has permission to perform the given action in the organization stored in the DB
240
func (p *Subscriptions) HasDBPermission(userEmail string, orgAddress common.Address, permission DBPermission) (bool, error) {
146✔
241
        user, err := p.db.UserByEmail(userEmail)
146✔
242
        if err != nil {
147✔
243
                return false, fmt.Errorf("could not get user: %v", err)
1✔
244
        }
1✔
245
        switch permission {
145✔
246
        case InviteUser, DeleteUser, CreateSubOrg:
30✔
247
                if !user.HasRoleFor(orgAddress, db.AdminRole) {
34✔
248
                        return false, errors.ErrUserHasNoAdminRole
4✔
249
                }
4✔
250
                return true, nil
26✔
251
        case CreateOrg:
115✔
252
                // Check if the user can create more organizations based on the MaxOrgsPerUser limit
115✔
253
                if len(user.Organizations) >= MaxOrgsPerUser {
115✔
254
                        return false, errors.ErrMaxOrganizationsReached.Withf(
×
255
                                "user is part of %d organizations, max allowed is %d",
×
256
                                len(user.Organizations),
×
257
                                MaxOrgsPerUser,
×
258
                        )
×
259
                }
×
260
                return true, nil
115✔
261
        default:
×
262
                return false, fmt.Errorf("permission not found")
×
263
        }
264
}
265

266
// OrgHasPermission checks if the org has permission to perform the given action
267
func (p *Subscriptions) OrgHasPermission(orgAddress common.Address, permission DBPermission) error {
13✔
268
        switch permission {
13✔
269
        case CreateDraft:
13✔
270
                // Check if the organization has a subscription
13✔
271
                org, err := p.db.Organization(orgAddress)
13✔
272
                if err != nil {
13✔
273
                        return errors.ErrOrganizationNotFound.WithErr(err)
×
274
                }
×
275

276
                // MaxDrafts value comes from the integrator's plan for managed orgs; the draft
277
                // count itself stays per-org.
278
                _, plan, err := p.limitsOwner(org)
13✔
279
                if err != nil {
13✔
280
                        return err
×
281
                }
×
282

283
                count, err := p.db.CountProcesses(orgAddress, db.DraftOnly)
13✔
284
                if err != nil {
13✔
285
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
286
                }
×
287

288
                if count >= int64(plan.Organization.MaxDrafts) {
16✔
289
                        return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
3✔
290
                }
3✔
291
                return nil
10✔
292
        default:
×
293
                return fmt.Errorf("permission not found")
×
294
        }
295
}
296

297
// OrgCanCreateVotingProcessDraft checks that the organization is under its MaxDrafts plan
298
// limit for the new /processes collection. It mirrors OrgHasPermission(CreateDraft) but
299
// counts votingProcesses drafts (the two collections have independent counts). MaxDrafts
300
// comes from the integrator's plan for a managed org; the draft count stays per-org.
301
func (p *Subscriptions) OrgCanCreateVotingProcessDraft(orgAddress common.Address) error {
24✔
302
        org, err := p.db.Organization(orgAddress)
24✔
303
        if err != nil {
24✔
NEW
304
                return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
305
        }
×
306
        _, plan, err := p.limitsOwner(org)
24✔
307
        if err != nil {
24✔
NEW
308
                return err
×
NEW
309
        }
×
310
        count, err := p.db.CountVotingProcesses(orgAddress, db.DraftOnly)
24✔
311
        if err != nil {
24✔
NEW
312
                return errors.ErrGenericInternalServerError.WithErr(err)
×
NEW
313
        }
×
314
        if count >= int64(plan.Organization.MaxDrafts) {
25✔
315
                return errors.ErrMaxDraftsReached.Withf("(%d)", plan.Organization.MaxDrafts)
1✔
316
        }
1✔
317
        return nil
23✔
318
}
319

320
// OrgAllowsVotingType checks that the organization's plan permits the given question ballot
321
// type. It maps the friendly type to the plan's VotingTypes feature flags. An empty type is
322
// allowed (it is validated elsewhere); a raw ballotProtocol override skips this check by
323
// passing an empty voteType. Weighted elections stay gated separately via CostFromWeight in
324
// hasElectionMetadataPermissions.
325
func (p *Subscriptions) OrgAllowsVotingType(orgAddress common.Address, voteType string) error {
24✔
326
        if voteType == "" {
25✔
327
                return nil
1✔
328
        }
1✔
329
        org, err := p.db.Organization(orgAddress)
23✔
330
        if err != nil {
23✔
NEW
331
                return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
332
        }
×
333
        _, plan, err := p.limitsOwner(org)
23✔
334
        if err != nil {
23✔
NEW
335
                return err
×
NEW
336
        }
×
337
        allowed := map[string]bool{
23✔
338
                db.VotingTypeSingleChoice: plan.VotingTypes.Single,
23✔
339
                db.VotingTypeMultiChoice:  plan.VotingTypes.Multiple,
23✔
340
        }
23✔
341
        ok, known := allowed[voteType]
23✔
342
        if !known {
24✔
343
                return errors.ErrInvalidData.Withf("unknown voting type %q", voteType)
1✔
344
        }
1✔
345
        if !ok {
23✔
346
                return errors.ErrVotingTypeNotAllowed.Withf("(%s)", voteType)
1✔
347
        }
1✔
348
        return nil
21✔
349
}
350

351
func (p *Subscriptions) OrgCanAddNMembers(orgAddress common.Address, memberNumber int) error {
69✔
352
        org, err := p.db.Organization(orgAddress)
69✔
353
        if err != nil {
69✔
354
                return errors.ErrOrganizationNotFound.WithErr(err)
×
355
        }
×
356

357
        owner, plan, err := p.limitsOwner(org)
69✔
358
        if err != nil {
70✔
359
                return err
1✔
360
        }
1✔
361

362
        // For a managed org the member limit is a shared pool across all of the integrator's
363
        // managed orgs; for a standalone org it is just the org's own members.
364
        var count int64
68✔
365
        if managed(org) {
70✔
366
                count, err = p.db.CountMembersManagedBy(owner.Address)
2✔
367
        } else {
68✔
368
                count, err = p.db.CountOrgMembers(orgAddress)
66✔
369
        }
66✔
370
        if err != nil {
68✔
371
                return errors.ErrGenericInternalServerError.WithErr(err)
×
372
        }
×
373

374
        if count+int64(memberNumber) > int64(plan.Organization.MaxCensus) {
72✔
375
                return errors.ErrExceedsOrganizationMembersLimit.Withf("(%d)", plan.Organization.MaxCensus)
4✔
376
        }
4✔
377
        return nil
64✔
378
}
379

380
func (p *Subscriptions) OrgCanPublishGroupCensus(census *db.Census, groupID string) error {
35✔
381
        org, err := p.db.Organization(census.OrgAddress)
35✔
382
        if err != nil {
35✔
383
                return errors.ErrOrganizationNotFound.WithErr(err)
×
384
        }
×
385
        group, err := p.db.OrganizationMemberGroup(groupID, org.Address)
35✔
386
        if err != nil {
35✔
387
                return errors.ErrGroupNotFound.WithErr(err)
×
388
        }
×
389
        memberCount := len(group.MemberIDs)
35✔
390
        if group.IsAutoGroup {
35✔
391
                count, err := p.db.CountOrgMembers(org.Address)
×
392
                if err != nil {
×
393
                        return errors.ErrGenericInternalServerError.WithErr(err)
×
394
                }
×
395
                memberCount = int(count)
×
396
        }
397
        // a legacy group census is a single election: one notification and one vote per member.
398
        return p.OrgCanPublishCensus(census, memberCount, memberCount)
35✔
399
}
400

401
// OrgCanPublishCensus enforces the org's remaining email/SMS/vote allowance. notifyCount is how
402
// many members would be notified (one 2FA challenge per voter), voteCount is how many votes would
403
// be cast (a multi-question /processes publishes N elections, so each voter can cast N ballots →
404
// voteCount = members × N while notifyCount stays members). It is the shared core of
405
// OrgCanPublishGroupCensus (legacy: notify == vote) and the inline /processes publish. For a
406
// managed org the allowance is the integrator's shared pool; for a standalone org its own counter.
407
func (p *Subscriptions) OrgCanPublishCensus(census *db.Census, notifyCount, voteCount int) error {
44✔
408
        org, err := p.db.Organization(census.OrgAddress)
44✔
409
        if err != nil {
44✔
NEW
410
                return errors.ErrOrganizationNotFound.WithErr(err)
×
NEW
411
        }
×
412
        owner, plan, err := p.limitsOwner(org)
44✔
413
        if err != nil {
44✔
NEW
414
                return err
×
NEW
415
        }
×
416

417
        // Only check (and, for managed orgs, aggregate) the 2FA channels the census actually
418
        // requests. For a managed org the allowance is a shared pool summed across all of the
419
        // integrator's managed orgs; for a standalone org it is the org's own sent counter.
420
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldEmail) {
75✔
421
                sentEmails := org.Counters.SentEmails
31✔
422
                if managed(org) {
32✔
423
                        if sentEmails, err = p.db.SumSentEmailsManagedBy(owner.Address); err != nil {
1✔
424
                                return errors.ErrGenericInternalServerError.WithErr(err)
×
425
                        }
×
426
                }
427
                if remainingEmails := max(0, plan.Features.TwoFaEmail-sentEmails); notifyCount > remainingEmails {
33✔
428
                        return errors.ErrProcessCensusSizeExceedsEmailAllowance.Withf("remaining emails: %d", remainingEmails)
2✔
429
                }
2✔
430
        }
431

432
        if census.TwoFaFields.Contains(db.OrgMemberTwoFaFieldPhone) {
58✔
433
                sentSMS := org.Counters.SentSMS
16✔
434
                if managed(org) {
17✔
435
                        if sentSMS, err = p.db.SumSentSMSManagedBy(owner.Address); err != nil {
1✔
436
                                return errors.ErrGenericInternalServerError.WithErr(err)
×
437
                        }
×
438
                }
439
                if remainingSMS := max(0, plan.Features.TwoFaSms-sentSMS); notifyCount > remainingSMS {
20✔
440
                        return errors.ErrProcessCensusSizeExceedsSMSAllowance.Withf("remaining sms: %d", remainingSMS)
4✔
441
                }
4✔
442
        }
443

444
        // Votes are metered for billing rather than blocked at cast time: an election may not be
445
        // published when its census size could exceed the remaining vote quota. As with 2FA, a
446
        // managed org draws on the integrator's shared pool (summed across its managed orgs); a
447
        // standalone org uses its own counter. MaxVotes of 0 means unlimited.
448
        if plan.Organization.MaxVotes > 0 {
42✔
449
                sentVotes := org.Counters.SentVotes
4✔
450
                if managed(org) {
7✔
451
                        if sentVotes, err = p.db.SumSentVotesManagedBy(owner.Address); err != nil {
3✔
452
                                return errors.ErrGenericInternalServerError.WithErr(err)
×
453
                        }
×
454
                }
455
                if remainingVotes := max(0, plan.Organization.MaxVotes-sentVotes); voteCount > remainingVotes {
6✔
456
                        return errors.ErrProcessCensusSizeExceedsVoteAllowance.Withf("remaining votes: %d", remainingVotes)
2✔
457
                }
2✔
458
        }
459

460
        return nil
36✔
461
}
462

463
// OrgCanPublishProcess enforces the synchronous plan denials for publishing one election —
464
// census size vs plan MaxCensus, per-org MaxProcesses count (non-managed), weighted allowance
465
// and duration — mirroring the NEW_PROCESS checks in HasTxPermission so the /processes publish
466
// path can surface them synchronously (as a 400 and in the dry-run) instead of as an opaque
467
// async job failure. The authoritative enforcement remains HasTxPermission at build time; this
468
// is an early, predictable subset. Anonymous/vote-overwrite are not used by the /processes flow.
469
// Admin role is verified by the caller.
470
//
471
//nolint:revive // weighted is an election attribute being validated, not a control flag
472
func (p *Subscriptions) OrgCanPublishProcess(
473
        org *db.Organization, maxCensusSize uint64, durationSeconds uint32, weighted bool,
474
) error {
9✔
475
        _, plan, err := p.limitsOwner(org)
9✔
476
        if err != nil {
9✔
NEW
477
                return err
×
NEW
478
        }
×
479
        if maxCensusSize > uint64(plan.Organization.MaxCensus) {
9✔
NEW
480
                return errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("plan max census: %d", plan.Organization.MaxCensus)
×
NEW
481
        }
×
482
        // The per-org process-count limit is enforced for standalone orgs; managed orgs draw on the
483
        // integrator's aggregate quota (see CanReserveManagedPublish). Test-sized elections are exempt.
484
        if !managed(org) && org.Counters.Processes >= plan.Organization.MaxProcesses &&
9✔
485
                maxCensusSize > uint64(db.TestMaxCensusSize) {
9✔
NEW
486
                return errors.ErrMaxProcessesReached
×
NEW
487
        }
×
488
        if weighted && !plan.VotingTypes.Weighted {
9✔
NEW
489
                return errors.ErrInvalidData.With("weighted elections are not allowed by the plan")
×
NEW
490
        }
×
491
        maxDuration := uint32(plan.Organization.MaxDuration * 24 * 60 * 60)
9✔
492
        if durationSeconds > maxDuration {
11✔
493
                return errors.ErrInvalidData.Withf("process duration exceeds the plan limit of %d days", plan.Organization.MaxDuration)
2✔
494
        }
2✔
495
        return nil
7✔
496
}
497

498
// CanReserveManagedPublish reports whether the integrator has remaining managed-process quota,
499
// WITHOUT reserving it — the read-only counterpart of db.ReserveManagedPublish, for the publish
500
// dry-run.
NEW
501
func (p *Subscriptions) CanReserveManagedPublish(integrator *db.Organization) error {
×
NEW
502
        maxProcesses, err := p.ManagedPublishLimits(integrator)
×
NEW
503
        if err != nil {
×
NEW
504
                return err
×
NEW
505
        }
×
NEW
506
        if integrator.Counters.ManagedProcesses >= maxProcesses {
×
NEW
507
                return errors.ErrIntegratorQuotaExceeded
×
NEW
508
        }
×
NEW
509
        return nil
×
510
}
511

512
func (p *Subscriptions) OrgCanAddCensusParticipants(orgAddress common.Address, censusID string, participantsCount int) error {
34✔
513
        org, err := p.db.Organization(orgAddress)
34✔
514
        if err != nil {
34✔
515
                return errors.ErrOrganizationNotFound.WithErr(err)
×
516
        }
×
517

518
        // Per-census size is bounded by the governing plan's MaxCensus: the integrator's plan
519
        // for a managed org, otherwise the org's own.
520
        _, plan, err := p.limitsOwner(org)
34✔
521
        if err != nil {
34✔
522
                return err
×
523
        }
×
524

525
        count, err := p.db.CountCensusParticipants(censusID)
34✔
526
        if err != nil {
34✔
527
                return errors.ErrGenericInternalServerError.WithErr(err)
×
528
        }
×
529

530
        if count+int64(participantsCount) > int64(plan.Organization.MaxCensus) {
36✔
531
                return errors.ErrProcessCensusSizeExceedsPlanLimit.Withf("(%d)", plan.Organization.MaxCensus)
2✔
532
        }
2✔
533
        return nil
32✔
534
}
535

536
// IsIntegrator reports whether the given organization is enabled as an integrator.
537
//
538
// Enablement is derived entirely from integrator limits — there is no separate flag:
539
//   - a per-organization IntegratorLimits override (the manual/admin path) grants
540
//     integrator status regardless of subscription state; and
541
//   - otherwise the organization's subscription plan grants it, but only while the
542
//     subscription is active.
543
//
544
// In both cases "integrator" means the effective limits allow at least one managed org.
545
func (p *Subscriptions) IsIntegrator(org *db.Organization) bool {
68✔
546
        if org == nil {
69✔
547
                return false
1✔
548
        }
1✔
549
        if org.IntegratorLimits != nil {
107✔
550
                return org.IntegratorLimits.MaxManagedOrgs > 0
40✔
551
        }
40✔
552
        if !org.Subscription.Active || org.Subscription.PlanID == "" {
31✔
553
                return false
4✔
554
        }
4✔
555
        plan, err := p.db.Plan(org.Subscription.PlanID)
23✔
556
        if err != nil || plan == nil {
23✔
557
                return false
×
558
        }
×
559
        return plan.IntegratorLimits.MaxManagedOrgs > 0
23✔
560
}
561

562
// EffectiveIntegratorLimits returns the integrator limits in force for the org: the
563
// per-organization override if set, otherwise the limits granted by its plan.
564
func (p *Subscriptions) EffectiveIntegratorLimits(org *db.Organization) (db.IntegratorLimits, error) {
32✔
565
        if org == nil {
33✔
566
                return db.IntegratorLimits{}, errors.ErrNotAnIntegrator
1✔
567
        }
1✔
568
        if org.IntegratorLimits != nil {
52✔
569
                return *org.IntegratorLimits, nil
21✔
570
        }
21✔
571
        if org.Subscription.PlanID == "" {
11✔
572
                return db.IntegratorLimits{}, errors.ErrPlanNotFound.With("organization has no subscription plan")
1✔
573
        }
1✔
574
        plan, err := p.db.Plan(org.Subscription.PlanID)
9✔
575
        if err != nil {
10✔
576
                return db.IntegratorLimits{}, fmt.Errorf("could not get subscription plan: %w", err)
1✔
577
        }
1✔
578
        return plan.IntegratorLimits, nil
8✔
579
}
580

581
// CanCreateManagedOrg checks that the integrator may create another managed organization.
582
func (p *Subscriptions) CanCreateManagedOrg(integrator *db.Organization) error {
14✔
583
        if !p.IsIntegrator(integrator) {
15✔
584
                return errors.ErrNotAnIntegrator
1✔
585
        }
1✔
586
        limits, err := p.EffectiveIntegratorLimits(integrator)
13✔
587
        if err != nil {
13✔
588
                return err
×
589
        }
×
590
        if integrator.Counters.ManagedOrgs >= limits.MaxManagedOrgs {
16✔
591
                return errors.ErrMaxManagedOrgsReached.Withf("limit %d", limits.MaxManagedOrgs)
3✔
592
        }
3✔
593
        return nil
10✔
594
}
595

596
// ManagedPublishLimits returns the integrator's aggregate cap for publishing under its
597
// managed organizations: the integrator plan's top-level process limit, which bounds the
598
// ManagedProcesses counter across all managed orgs.
599
func (p *Subscriptions) ManagedPublishLimits(integrator *db.Organization) (maxProcesses int, err error) {
8✔
600
        if !p.IsIntegrator(integrator) {
9✔
601
                return 0, errors.ErrNotAnIntegrator
1✔
602
        }
1✔
603
        if integrator.Subscription.PlanID == "" {
7✔
604
                return 0, errors.ErrPlanNotFound.With("integrator has no subscription plan")
×
605
        }
×
606
        plan, err := p.db.Plan(integrator.Subscription.PlanID)
7✔
607
        if err != nil {
7✔
608
                if stderrors.Is(err, db.ErrNotFound) {
×
609
                        return 0, errors.ErrPlanNotFound.WithErr(err)
×
610
                }
×
611
                return 0, errors.ErrGenericInternalServerError.WithErr(err)
×
612
        }
613
        return plan.Organization.MaxProcesses, nil
7✔
614
}
615

616
// CanPublishForManagedOrg checks the integrator's aggregate process quota before publishing
617
// an election under a managed org.
618
func (p *Subscriptions) CanPublishForManagedOrg(integrator *db.Organization) error {
3✔
619
        maxProcesses, err := p.ManagedPublishLimits(integrator)
3✔
620
        if err != nil {
4✔
621
                return err
1✔
622
        }
1✔
623
        if integrator.Counters.ManagedProcesses >= maxProcesses {
3✔
624
                return errors.ErrIntegratorQuotaExceeded.Withf("max managed processes %d", maxProcesses)
1✔
625
        }
1✔
626
        return nil
1✔
627
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc