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

vocdoni / saas-backend / 21511535617

30 Jan 2026 09:33AM UTC coverage: 63.154% (-0.1%) from 63.261%
21511535617

Pull #410

github

altergui
Update docs for period usage
Pull Request #410: fix(subscriptions): implement annual counters

132 of 195 new or added lines in 7 files covered. (67.69%)

217 existing lines in 7 files now uncovered.

7233 of 11453 relevant lines covered (63.15%)

39.95 hits per line

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

72.86
/api/organizations.go
1
package api
2

3
import (
4
        "encoding/json"
5
        "net/http"
6
        "time"
7

8
        "github.com/ethereum/go-ethereum/common"
9
        "github.com/vocdoni/saas-backend/account"
10
        "github.com/vocdoni/saas-backend/api/apicommon"
11
        "github.com/vocdoni/saas-backend/db"
12
        "github.com/vocdoni/saas-backend/errors"
13
        "github.com/vocdoni/saas-backend/internal"
14
        "github.com/vocdoni/saas-backend/notifications/mailtemplates"
15
        "github.com/vocdoni/saas-backend/subscriptions"
16
        "go.vocdoni.io/dvote/log"
17
)
18

19
// createOrganizationHandler godoc
20
//
21
//        @Summary                Create a new organization
22
//        @Description        Create a new organization. If the organization is a suborganization, the parent organization must be
23
//        @Description        specified in the request body, and the user must be an admin of the parent. If the parent organization
24
//        @Description        is already a suborganization, an error is returned.
25
//        @Tags                        organizations
26
//        @Accept                        json
27
//        @Produce                json
28
//        @Security                BearerAuth
29
//        @Param                        request        body                apicommon.OrganizationInfo        true        "Organization information"
30
//        @Success                200                {object}        apicommon.OrganizationInfo
31
//        @Failure                400                {object}        errors.Error        "Invalid input data"
32
//        @Failure                401                {object}        errors.Error        "Unauthorized"
33
//        @Failure                404                {object}        errors.Error        "Parent organization not found"
34
//        @Failure                500                {object}        errors.Error        "Internal server error"
35
//        @Router                        /organizations [post]
36
func (a *API) createOrganizationHandler(w http.ResponseWriter, r *http.Request) {
58✔
37
        // get the user from the request context
58✔
38
        user, ok := apicommon.UserFromContext(r.Context())
58✔
39
        if !ok {
58✔
40
                errors.ErrUnauthorized.Write(w)
×
41
                return
×
42
        }
×
43
        // get the organization info from the request body
44
        orgInfo := &apicommon.OrganizationInfo{}
58✔
45
        if err := json.NewDecoder(r.Body).Decode(orgInfo); err != nil {
59✔
46
                errors.ErrMalformedBody.Write(w)
1✔
47
                return
1✔
48
        }
1✔
49
        // create the organization signer to store the address and the nonce
50
        // TODO: replace email with something else such as user ID
51
        signer, nonce, err := account.NewSigner(a.secret, user.Email)
57✔
52
        if err != nil {
57✔
53
                errors.ErrGenericInternalServerError.Withf("could not create organization signer: %v", err).Write(w)
×
54
                return
×
55
        }
×
56
        // check if the organization type is valid
57
        if !db.IsOrganizationTypeValid(orgInfo.Type) {
58✔
58
                errors.ErrMalformedBody.Withf("invalid organization type").Write(w)
1✔
59
                return
1✔
60
        }
1✔
61
        // find default plan
62
        defaultPlan, err := a.db.DefaultPlan()
56✔
63
        if err != nil || defaultPlan == nil {
56✔
64
                errors.ErrNoDefaultPlan.WithErr((err)).Write(w)
×
65
                return
×
66
        }
×
67
        parentOrg := common.Address{}
56✔
68
        var dbParentOrg *db.Organization
56✔
69
        if orgInfo.Parent != nil {
59✔
70
                // check if the org has permission to create suborganizations
3✔
71
                hasPermission, err := a.subscriptions.HasDBPermission(user.Email, orgInfo.Parent.Address, subscriptions.CreateSubOrg)
3✔
72
                if !hasPermission || err != nil {
4✔
73
                        errors.ErrUnauthorized.Withf("user does not have permission to create suborganizations: %v", err).Write(w)
1✔
74
                        return
1✔
75
                }
1✔
76

77
                dbParentOrg, err = a.db.Organization(orgInfo.Parent.Address)
2✔
78
                if err != nil {
2✔
79
                        if err == db.ErrNotFound {
×
80
                                errors.ErrOrganizationNotFound.Withf("parent organization not found").Write(w)
×
81
                                return
×
82
                        }
×
83
                        errors.ErrGenericInternalServerError.Withf("could not get parent organization: %v", err).Write(w)
×
84
                        return
×
85
                }
86
                if len(dbParentOrg.Parent) > 0 {
4✔
87
                        errors.ErrMalformedBody.Withf("parent organization is already a suborganization").Write(w)
2✔
88
                        return
2✔
89
                }
2✔
90
                isAdmin, err := a.db.UserHasRoleInOrg(user.Email, dbParentOrg.Address, db.AdminRole)
×
91
                if err != nil {
×
92
                        errors.ErrGenericInternalServerError.
×
93
                                Withf("could not check if user is admin of parent organization: %v", err).
×
94
                                Write(w)
×
95
                        return
×
96
                }
×
97
                if !isAdmin {
×
98
                        errors.ErrUnauthorized.Withf("user is not admin of parent organization").Write(w)
×
99
                        return
×
100
                }
×
101
                parentOrg = orgInfo.Parent.Address
×
102
                // update the parent organization counter
×
103
                if err := a.db.IncrementOrganizationSubOrgsCounter(parentOrg); err != nil {
×
104
                        errors.ErrGenericInternalServerError.Withf("increment suborgs: %v", err).Write(w)
×
105
                        return
×
106
                }
×
107
        }
108
        // create the organization
109
        dbOrg := &db.Organization{
53✔
110
                Address:         signer.Address(),
53✔
111
                Website:         orgInfo.Website,
53✔
112
                Creator:         user.Email,
53✔
113
                CreatedAt:       time.Now(),
53✔
114
                Nonce:           nonce,
53✔
115
                Type:            db.OrganizationType(orgInfo.Type),
53✔
116
                Size:            orgInfo.Size,
53✔
117
                Color:           orgInfo.Color,
53✔
118
                Country:         orgInfo.Country,
53✔
119
                Subdomain:       orgInfo.Subdomain,
53✔
120
                Timezone:        orgInfo.Timezone,
53✔
121
                Active:          true,
53✔
122
                Communications:  orgInfo.Communications,
53✔
123
                TokensPurchased: 0,
53✔
124
                TokensRemaining: 0,
53✔
125
                Parent:          parentOrg,
53✔
126
                Subscription: db.OrganizationSubscription{
53✔
127
                        PlanID:    defaultPlan.ID,
53✔
128
                        StartDate: time.Now(),
53✔
129
                        Active:    true,
53✔
130
                },
53✔
131
        }
53✔
132
        if err := a.db.SetOrganization(dbOrg); err != nil {
53✔
133
                if orgInfo.Parent != nil {
×
134
                        if err := a.db.DecrementOrganizationSubOrgsCounter(parentOrg); err != nil {
×
135
                                log.Errorf("decrement suborgs: %v", err)
×
136
                        }
×
137
                }
138
                if err == db.ErrAlreadyExists {
×
139
                        errors.ErrInvalidOrganizationData.WithErr(err).Write(w)
×
140
                        return
×
141
                }
×
142
                errors.ErrGenericInternalServerError.Write(w)
×
143
                return
×
144
        }
145
        // send the organization back to the user
146
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationFromDB(dbOrg, dbParentOrg))
53✔
147
}
148

149
// organizationInfoHandler godoc
150
//
151
//        @Summary                Get organization information
152
//        @Description        Get information about an organization
153
//        @Tags                        organizations
154
//        @Accept                        json
155
//        @Produce                json
156
//        @Param                        address        path                string        true        "Organization address"
157
//        @Success                200                {object}        apicommon.OrganizationInfo
158
//        @Failure                400                {object}        errors.Error        "Invalid input data"
159
//        @Failure                404                {object}        errors.Error        "Organization not found"
160
//        @Failure                500                {object}        errors.Error        "Internal server error"
161
//        @Router                        /organizations/{address} [get]
162
func (a *API) organizationInfoHandler(w http.ResponseWriter, r *http.Request) {
60✔
163
        // get the organization info from the request context
60✔
164
        org, parent, ok := a.organizationFromRequest(r)
60✔
165
        if !ok {
61✔
166
                errors.ErrNoOrganizationProvided.Write(w)
1✔
167
                return
1✔
168
        }
1✔
169
        // send the organization back to the user
170
        apicommon.HTTPWriteJSON(w, apicommon.OrganizationFromDB(org, parent))
59✔
171
}
172

173
// updateOrganizationHandler godoc
174
//
175
//        @Summary                Update organization information
176
//        @Description        Update the information of an organization.
177
//        @Description        Only the admin of the organization can update the information.
178
//        @Description        Only certain fields can be updated, and they will be updated only if they are not empty.
179
//        @Tags                        organizations
180
//        @Accept                        json
181
//        @Produce                json
182
//        @Security                BearerAuth
183
//        @Param                        address        path                string                                                true        "Organization address"
184
//        @Param                        request        body                apicommon.OrganizationInfo        true        "Organization information to update"
185
//        @Success                200                {string}        string                                                "OK"
186
//        @Failure                400                {object}        errors.Error                                "Invalid input data"
187
//        @Failure                401                {object}        errors.Error                                "Unauthorized"
188
//        @Failure                404                {object}        errors.Error                                "Organization not found"
189
//        @Failure                500                {object}        errors.Error                                "Internal server error"
190
//        @Router                        /organizations/{address} [put]
191
func (a *API) updateOrganizationHandler(w http.ResponseWriter, r *http.Request) {
6✔
192
        // get the user from the request context
6✔
193
        user, ok := apicommon.UserFromContext(r.Context())
6✔
194
        if !ok {
6✔
195
                errors.ErrUnauthorized.Write(w)
×
196
                return
×
197
        }
×
198
        // get the organization info from the request context
199
        org, _, ok := a.organizationFromRequest(r)
6✔
200
        if !ok {
7✔
201
                errors.ErrNoOrganizationProvided.Write(w)
1✔
202
                return
1✔
203
        }
1✔
204
        if !user.HasRoleFor(org.Address, db.AdminRole) {
6✔
205
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
206
                return
1✔
207
        }
1✔
208
        // get the organization info from the request body
209
        newOrgInfo := &apicommon.OrganizationInfo{}
4✔
210
        if err := json.NewDecoder(r.Body).Decode(newOrgInfo); err != nil {
5✔
211
                errors.ErrMalformedBody.Write(w)
1✔
212
                return
1✔
213
        }
1✔
214
        // update just the fields that can be updated and are not empty
215
        updateOrg := false
3✔
216
        if newOrgInfo.Website != "" {
5✔
217
                org.Website = newOrgInfo.Website
2✔
218
                updateOrg = true
2✔
219
        }
2✔
220
        if newOrgInfo.Size != "" {
4✔
221
                org.Size = newOrgInfo.Size
1✔
222
                updateOrg = true
1✔
223
        }
1✔
224
        if newOrgInfo.Color != "" {
4✔
225
                org.Color = newOrgInfo.Color
1✔
226
                updateOrg = true
1✔
227
        }
1✔
228
        if newOrgInfo.Subdomain != "" {
4✔
229
                org.Subdomain = newOrgInfo.Subdomain
1✔
230
                updateOrg = true
1✔
231
        }
1✔
232
        if newOrgInfo.Country != "" {
4✔
233
                org.Country = newOrgInfo.Country
1✔
234
                updateOrg = true
1✔
235
        }
1✔
236
        if newOrgInfo.Timezone != "" {
4✔
237
                org.Timezone = newOrgInfo.Timezone
1✔
238
                updateOrg = true
1✔
239
        }
1✔
240
        if newOrgInfo.Active != org.Active {
5✔
241
                org.Active = newOrgInfo.Active
2✔
242
                updateOrg = true
2✔
243
        }
2✔
244
        // update the organization if any field was changed
245
        if updateOrg {
5✔
246
                if err := a.db.SetOrganization(org); err != nil {
2✔
247
                        errors.ErrGenericInternalServerError.Withf("could not update organization: %v", err).Write(w)
×
248
                        return
×
249
                }
×
250
        }
251
        apicommon.HTTPWriteOK(w)
3✔
252
}
253

254
// organizationsTypesHandler godoc
255
//
256
//        @Summary                Get available organization types
257
//        @Description        Get the list of available organization types that can be assigned to an organization
258
//        @Tags                        organizations
259
//        @Accept                        json
260
//        @Produce                json
261
//        @Success                200        {object}        apicommon.OrganizationTypeList
262
//        @Router                        /organizations/types [get]
263
func (*API) organizationsTypesHandler(w http.ResponseWriter, _ *http.Request) {
1✔
264
        organizationTypes := []*apicommon.OrganizationType{}
1✔
265
        for orgType, name := range db.OrganizationTypesNames {
10✔
266
                organizationTypes = append(organizationTypes, &apicommon.OrganizationType{
9✔
267
                        Type: string(orgType),
9✔
268
                        Name: name,
9✔
269
                })
9✔
270
        }
9✔
271
        apicommon.HTTPWriteJSON(w, &apicommon.OrganizationTypeList{Types: organizationTypes})
1✔
272
}
273

274
// organizationSubscriptionHandler godoc
275
//
276
//        @Summary                Get organization subscription
277
//        @Description        Get the subscription information for an organization
278
//        @Tags                        organizations
279
//        @Accept                        json
280
//        @Produce                json
281
//        @Security                BearerAuth
282
//        @Param                        address        path                string        true        "Organization address"
283
//        @Success                200                {object}        apicommon.OrganizationSubscriptionInfo
284
//        @Failure                400                {object}        errors.Error        "Invalid input data"
285
//        @Failure                401                {object}        errors.Error        "Unauthorized"
286
//        @Failure                404                {object}        errors.Error        "Organization not found or no subscription"
287
//        @Failure                500                {object}        errors.Error        "Internal server error"
288
//        @Router                        /organizations/{address}/subscription [get]
289
func (a *API) organizationSubscriptionHandler(w http.ResponseWriter, r *http.Request) {
3✔
290
        // get the user from the request context
3✔
291
        user, ok := apicommon.UserFromContext(r.Context())
3✔
292
        if !ok {
3✔
293
                errors.ErrUnauthorized.Write(w)
×
294
                return
×
295
        }
×
296
        // get the organization info from the request context
297
        org, _, ok := a.organizationFromRequest(r)
3✔
298
        if !ok {
4✔
299
                errors.ErrNoOrganizationProvided.Write(w)
1✔
300
                return
1✔
301
        }
1✔
302
        if !user.HasRoleFor(org.Address, db.AdminRole) {
3✔
303
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
304
                return
1✔
305
        }
1✔
306
        if org.Subscription == (db.OrganizationSubscription{}) {
1✔
307
                errors.ErrOrganizationHasNoSubscription.Write(w)
×
308
                return
×
309
        }
×
310
        // get the subscription from the database
311
        plan, err := a.db.Plan(org.Subscription.PlanID)
1✔
312
        if err != nil {
1✔
313
                errors.ErrGenericInternalServerError.Withf("could not get subscription: %v", err).Write(w)
×
314
                return
×
315
        }
×
316
        info := &apicommon.OrganizationSubscriptionInfo{
1✔
317
                SubscriptionDetails: apicommon.SubscriptionDetailsFromDB(&org.Subscription),
1✔
318
                Usage:               apicommon.SubscriptionUsageFromDB(&org.Counters),
1✔
319
                Plan:                apicommon.SubscriptionPlanFromDB(plan),
1✔
320
        }
1✔
321
        if periodUsage, ok, err := a.subscriptions.PeriodUsage(org); err != nil {
1✔
NEW
322
                errors.ErrGenericInternalServerError.Withf("could not get period usage: %v", err).Write(w)
×
NEW
323
                return
×
324
        } else if ok {
2✔
325
                usage := apicommon.SubscriptionUsageFromDB(&periodUsage)
1✔
326
                info.PeriodUsage = &usage
1✔
327
        }
1✔
328
        apicommon.HTTPWriteJSON(w, info)
1✔
329
}
330

331
// organizationCensusesHandler godoc
332
//
333
//        @Summary                Get organization censuses
334
//        @Description        Get the list of censuses for an organization
335
//        @Tags                        organizations
336
//        @Accept                        json
337
//        @Produce                json
338
//        @Security                BearerAuth
339
//        @Param                        address        path                string        true        "Organization address"
340
//        @Success                200                {object}        apicommon.OrganizationCensuses
341
//        @Failure                400                {object}        errors.Error        "Invalid input data"
342
//        @Failure                401                {object}        errors.Error        "Unauthorized"
343
//        @Failure                404                {object}        errors.Error        "Organization not found"
344
//        @Failure                500                {object}        errors.Error        "Internal server error"
345
//        @Router                        /organizations/{address}/censuses [get]
346
func (a *API) organizationCensusesHandler(w http.ResponseWriter, r *http.Request) {
3✔
347
        // get the user from the request context
3✔
348
        user, ok := apicommon.UserFromContext(r.Context())
3✔
349
        if !ok {
3✔
350
                errors.ErrUnauthorized.Write(w)
×
351
                return
×
352
        }
×
353
        // get the organization info from the request context
354
        org, _, ok := a.organizationFromRequest(r)
3✔
355
        if !ok {
4✔
356
                errors.ErrNoOrganizationProvided.Write(w)
1✔
357
                return
1✔
358
        }
1✔
359
        if !user.HasRoleFor(org.Address, db.AdminRole) {
3✔
360
                errors.ErrUnauthorized.Withf("user is not admin of organization").Write(w)
1✔
361
                return
1✔
362
        }
1✔
363
        // get the censuses from the database
364
        censuses, err := a.db.CensusesByOrg(org.Address)
1✔
365
        if err != nil {
1✔
366
                if err == db.ErrNotFound {
×
367
                        errors.ErrOrganizationNotFound.Write(w)
×
368
                        return
×
369
                }
×
370
                errors.ErrGenericInternalServerError.Withf("could not get censuses: %v", err).Write(w)
×
371
                return
×
372
        }
373
        // decode the censuses from the database
374
        result := apicommon.OrganizationCensuses{
1✔
375
                Censuses: []apicommon.OrganizationCensus{},
1✔
376
        }
1✔
377
        for _, census := range censuses {
1✔
378
                result.Censuses = append(result.Censuses, apicommon.OrganizationCensusFromDB(census))
×
379
        }
×
380
        apicommon.HTTPWriteJSON(w, result)
1✔
381
}
382

383
// organizationCreateTicket godoc
384
//
385
//        @Summary                Create a new ticket for an organization
386
//        @Description        Create a new ticket for an organization. The user must have some role in the organization (any role).
387
//        @Tags                        organizations
388
//        @Accept                        json
389
//        @Produce                json
390
//        @Security                BearerAuth
391
//        @Param                        address        path                string                                                                                true        "Organization address"
392
//        @Param                        request        body                apicommon.CreateOrganizationTicketRequest        true        "Ticket request information"
393
//        @Success                200                {string}        string                                                                                "OK"
394
//        @Failure                400                {object}        errors.Error                                                                "Invalid input data"
395
//        @Failure                401                {object}        errors.Error                                                                "Unauthorized"
396
//        @Failure                404                {object}        errors.Error                                                                "Organization not found"
397
//        @Failure                500                {object}        errors.Error                                                                "Internal server error"
398
//        @Router                        /organizations/{address}/ticket [post]
399
func (a *API) organizationCreateTicket(w http.ResponseWriter, r *http.Request) {
6✔
400
        // get the user from the request context
6✔
401
        user, ok := apicommon.UserFromContext(r.Context())
6✔
402
        if !ok {
6✔
403
                errors.ErrUnauthorized.Write(w)
×
404
                return
×
405
        }
×
406
        // get the organization info from the request context
407
        org, _, ok := a.organizationFromRequest(r)
6✔
408
        if !ok {
7✔
409
                errors.ErrNoOrganizationProvided.Write(w)
1✔
410
                return
1✔
411
        }
1✔
412
        // check if the new user already has a role in the organization
413
        if hasAnyRole, err := a.db.UserHasAnyRoleInOrg(user.Email, org.Address); err != nil {
5✔
414
                errors.ErrInvalidUserData.WithErr(err).Write(w)
×
415
                return
×
416
        } else if !hasAnyRole {
6✔
417
                errors.ErrUnauthorized.Withf("user has no role in the organization").Write(w)
1✔
418
                return
1✔
419
        }
1✔
420

421
        // get the ticket request from the request body
422
        ticketReq := &apicommon.CreateOrganizationTicketRequest{}
4✔
423
        if err := json.NewDecoder(r.Body).Decode(ticketReq); err != nil {
5✔
424
                errors.ErrMalformedBody.Write(w)
1✔
425
                return
1✔
426
        }
1✔
427
        // validate the ticket request
428
        if ticketReq.Title == "" || ticketReq.Description == "" {
5✔
429
                errors.ErrMalformedBody.With("title and description are required").Write(w)
2✔
430
                return
2✔
431
        }
2✔
432

433
        if !internal.ValidEmail(user.Email) {
1✔
434
                errors.ErrEmailMalformed.With("invalid user email address").Write(w)
×
435
                return
×
436
        }
×
437
        lang := a.getLanguageFromContext(r.Context())
1✔
438
        notification, err := mailtemplates.SupportNotification.Localized(lang).ExecTemplate(
1✔
439
                struct {
1✔
440
                        Type         string
1✔
441
                        Organization common.Address
1✔
442
                        Title        string
1✔
443
                        Description  string
1✔
444
                        Email        string
1✔
445
                }{ticketReq.TicketType, org.Address, ticketReq.Title, ticketReq.Description, user.Email},
1✔
446
        )
1✔
447
        if err != nil {
1✔
448
                log.Warnw("could not execute support notification template", "error", err)
×
449
                errors.ErrGenericInternalServerError.Write(w)
×
450
                return
×
451
        }
×
452

453
        notification.ToAddress = apicommon.SupportEmail
1✔
454
        notification.ReplyTo = user.Email
1✔
455
        notification.CCAddress = user.Email
1✔
456

1✔
457
        // send an email to the support destination
1✔
458
        if err := a.mail.SendNotification(r.Context(), notification); err != nil {
1✔
459
                log.Warnw("could not send ticket notification email", "error", err)
×
460
                errors.ErrGenericInternalServerError.Write(w)
×
461
                return
×
462
        }
×
463
        apicommon.HTTPWriteOK(w)
1✔
464
}
465

466
// organizationJobsHandler godoc
467
//
468
//        @Summary                Get organization jobs
469
//        @Description        Get the list of import jobs for an organization with pagination support
470
//        @Tags                        organizations
471
//        @Accept                        json
472
//        @Produce                json
473
//        @Security                BearerAuth
474
//        @Param                        address        path                string        true        "Organization address"
475
//        @Param                        page        query                integer        false        "Page number (default: 1)"
476
//        @Param                        limit        query                integer        false        "Number of items per page (default: 10)"
477
//        @Param                        type        query                string        false        "Filter by job type (org_members or census_participants)"
478
//        @Success                200                {object}        apicommon.JobsResponse
479
//        @Failure                400                {object}        errors.Error        "Invalid input"
480
//        @Failure                401                {object}        errors.Error        "Unauthorized"
481
//        @Failure                404                {object}        errors.Error        "Organization not found"
482
//        @Failure                500                {object}        errors.Error        "Internal server error"
483
//        @Router                        /organizations/{address}/jobs [get]
484
func (a *API) organizationJobsHandler(w http.ResponseWriter, r *http.Request) {
24✔
485
        // get the user from the request context
24✔
486
        user, ok := apicommon.UserFromContext(r.Context())
24✔
487
        if !ok {
24✔
488
                errors.ErrUnauthorized.Write(w)
×
489
                return
×
490
        }
×
491
        // get the organization info from the request context
492
        org, _, ok := a.organizationFromRequest(r)
24✔
493
        if !ok {
27✔
494
                errors.ErrNoOrganizationProvided.Write(w)
3✔
495
                return
3✔
496
        }
3✔
497
        // check the user has the necessary permissions
498
        if !user.HasRoleFor(org.Address, db.ManagerRole) && !user.HasRoleFor(org.Address, db.AdminRole) {
22✔
499
                errors.ErrUnauthorized.Withf("user is not admin or manager of organization").Write(w)
1✔
500
                return
1✔
501
        }
1✔
502

503
        // Parse job type filter
504
        var jobType *db.JobType
20✔
505
        if typeStr := r.URL.Query().Get("type"); typeStr != "" {
29✔
506
                switch typeStr {
9✔
507
                case string(db.JobTypeOrgMembers):
3✔
508
                        t := db.JobTypeOrgMembers
3✔
509
                        jobType = &t
3✔
510
                case string(db.JobTypeCensusParticipants):
3✔
511
                        t := db.JobTypeCensusParticipants
3✔
512
                        jobType = &t
3✔
513
                default:
3✔
514
                        errors.ErrMalformedURLParam.Withf("invalid job type: %s", typeStr).Write(w)
3✔
515
                        return
3✔
516
                }
517
        }
518

519
        params, err := parsePaginationParams(r.URL.Query().Get(ParamPage), r.URL.Query().Get(ParamLimit))
17✔
520
        if err != nil {
19✔
521
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
2✔
522
                return
2✔
523
        }
2✔
524
        totalItems, jobs, err := a.db.Jobs(org.Address, params.Page, params.Limit, jobType)
15✔
525
        if err != nil {
15✔
526
                errors.ErrGenericInternalServerError.Withf("could not get jobs: %v", err).Write(w)
×
527
                return
×
528
        }
×
529
        pagination, err := calculatePagination(params.Page, params.Limit, totalItems)
15✔
530
        if err != nil {
15✔
531
                errors.ErrMalformedURLParam.WithErr(err).Write(w)
×
532
                return
×
533
        }
×
534

535
        // convert the jobs to the response format
536
        jobsResponse := make([]apicommon.JobInfo, 0, len(jobs))
15✔
537
        for _, job := range jobs {
25✔
538
                jobsResponse = append(jobsResponse, apicommon.JobFromDB(&job))
10✔
539
        }
10✔
540

541
        apicommon.HTTPWriteJSON(w, &apicommon.JobsResponse{
15✔
542
                Pagination: pagination,
15✔
543
                Jobs:       jobsResponse,
15✔
544
        })
15✔
545
}
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