Skip to content
Merged
6 changes: 6 additions & 0 deletions cmd/committee-api/design/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,12 @@ var CommitteeInviteWithReadonlyAttributes = dsl.Type("committee-invite-with-read
dsl.Example("7cad5a8d-19d0-41a4-81a6-043453daf9ee")
dsl.Format(dsl.FormatUUID)
})
dsl.Attribute("committee_name", dsl.String, "Name of the committee at the time the invite was created", func() {
dsl.Example("Technical Steering Committee")
})
dsl.Attribute("organization_required", dsl.Boolean, "Whether the invitee must supply an organization when accepting. True when the committee has voting enabled or requires a business email.", func() {
dsl.Example(false)
})
dsl.Attribute("invitee_email", dsl.String, "Email of the invited person", func() {
dsl.Format(dsl.FormatEmail)
dsl.Example("invitee@example.com")
Expand Down
43 changes: 37 additions & 6 deletions cmd/committee-api/service/committee_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ func (s *committeeServicesrvc) GetInvite(ctx context.Context, p *committeeservic
return nil, wrapError(ctx, errors.NewNotFound("invite not found in this committee"))
}

s.enrichInviteFromCommittee(ctx, invite, p.UID)

return s.convertInviteDomainToResponse(invite), nil
Comment thread
andrest50 marked this conversation as resolved.
}

Expand All @@ -657,6 +659,10 @@ func (s *committeeServicesrvc) CreateInvite(ctx context.Context, p *committeeser
return nil, wrapError(ctx, err)
}

// Best-effort: settings drive organization_required; missing settings means false.
committeeSettings, _, _ := s.storage.GetSettings(ctx, p.UID)
orgRequired := committeeBase.EnableVoting || (committeeSettings != nil && committeeSettings.BusinessEmailRequired)
Comment thread
andrest50 marked this conversation as resolved.

var inviteOrgID, inviteOrgName, inviteOrgWebsite *string
if p.Organization != nil {
inviteOrgID = p.Organization.ID
Expand All @@ -666,12 +672,14 @@ func (s *committeeServicesrvc) CreateInvite(ctx context.Context, p *committeeser
inviteOrganization := organizationPtrFromFields(inviteOrgID, inviteOrgName, inviteOrgWebsite)

invite := &model.CommitteeInvite{
UID: uuid.New().String(),
CommitteeUID: p.UID,
InviteeEmail: p.InviteeEmail,
Organization: inviteOrganization,
Status: "pending",
CreatedAt: time.Now().UTC(),
UID: uuid.New().String(),
CommitteeUID: p.UID,
CommitteeName: committeeBase.Name,
OrganizationRequired: orgRequired,
InviteeEmail: p.InviteeEmail,
Organization: inviteOrganization,
Status: "pending",
CreatedAt: time.Now().UTC(),
Comment thread
andrest50 marked this conversation as resolved.
}
if p.Role != nil {
invite.Role = *p.Role
Expand Down Expand Up @@ -707,6 +715,8 @@ func (s *committeeServicesrvc) CreateInvite(ctx context.Context, p *committeeser
return nil, wrapError(ctx, errGet)
}
revokedInvite.Status = "pending"
revokedInvite.CommitteeName = committeeBase.Name
revokedInvite.OrganizationRequired = orgRequired
if p.Role != nil {
revokedInvite.Role = *p.Role
}
Expand Down Expand Up @@ -855,6 +865,7 @@ func (s *committeeServicesrvc) RevokeInvite(ctx context.Context, p *committeeser
}

invite.Status = "revoked"
s.enrichInviteFromCommittee(ctx, invite, p.UID)
if err := s.storage.UpdateInvite(ctx, invite, rev); err != nil {
return wrapError(ctx, err)
}
Expand Down Expand Up @@ -923,6 +934,7 @@ func (s *committeeServicesrvc) AcceptInvite(ctx context.Context, p *committeeser

// Member created successfully — now mark the invite accepted.
invite.Status = "accepted"
s.enrichInviteFromCommittee(ctx, invite, p.UID)
if err := s.storage.UpdateInvite(ctx, invite, rev); err != nil {
return nil, wrapError(ctx, err)
}
Expand Down Expand Up @@ -966,6 +978,7 @@ func (s *committeeServicesrvc) DeclineInvite(ctx context.Context, p *committeese
}

invite.Status = "declined"
s.enrichInviteFromCommittee(ctx, invite, p.UID)
if err := s.storage.UpdateInvite(ctx, invite, rev); err != nil {
return nil, wrapError(ctx, err)
}
Expand Down Expand Up @@ -1311,6 +1324,24 @@ func (s *committeeServicesrvc) resolveCallerEmail(ctx context.Context) (string,
return userEmails.PrimaryEmail, nil
}

// enrichInviteFromCommittee populates invite fields derived from the committee.
// It sets CommitteeName when missing and always refreshes OrganizationRequired from
// the committee's current settings (voting enabled or business email required).
// Best-effort: errors are logged and the invite is left unchanged on failure.
func (s *committeeServicesrvc) enrichInviteFromCommittee(ctx context.Context, invite *model.CommitteeInvite, committeeUID string) {
cb, _, err := s.storage.GetBase(ctx, committeeUID)
if err != nil {
slog.WarnContext(ctx, "enrichInviteFromCommittee: failed to get committee base",
"committee_uid", committeeUID, "error", err)
return
}
if invite.CommitteeName == "" {
invite.CommitteeName = cb.Name
}
settings, _, _ := s.storage.GetSettings(ctx, committeeUID)
invite.OrganizationRequired = cb.EnableVoting || (settings != nil && settings.BusinessEmailRequired)
}

// publishInviteIndexerMessage publishes an indexer message for invite operations.
// Publishing is best-effort: failures are logged but do not fail the request.
// IndexingConfig is required because the indexer is data-agnostic; publishers supply all indexing metadata.
Expand Down
4 changes: 4 additions & 0 deletions cmd/committee-api/service/committee_service_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,10 @@ func (s *committeeServicesrvc) convertInviteDomainToResponse(invite *model.Commi
InviteeEmail: &invite.InviteeEmail,
Status: invite.Status,
}
if invite.CommitteeName != "" {
result.CommitteeName = &invite.CommitteeName
}
result.OrganizationRequired = &invite.OrganizationRequired
if invite.Role != "" {
result.Role = &invite.Role
}
Expand Down
2 changes: 2 additions & 0 deletions docs/indexer-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ _(none)_
|---|---|---|
| `uid` | string | Invite unique identifier |
| `committee_uid` | string | UID of the committee this invite belongs to |
| `committee_name` | string | Name of the committee at the time the invite was created |
| `organization_required` | boolean | True when the committee has voting enabled or requires a business email; the UI uses this to decide whether to prompt for an organization on accept |
| `invitee_email` | string | Email address of the invitee |
| `role` | string | Role the invitee is being invited to |
| `organization` | object | Organization for the invitee (`id`, `name`, `website`) when provided on create |
Expand Down
5 changes: 5 additions & 0 deletions gen/committee_service/service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gen/http/cli/committee/cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions gen/http/committee_service/client/cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 37 additions & 16 deletions gen/http/committee_service/client/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 39 additions & 18 deletions gen/http/committee_service/server/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gen/http/openapi.json

Large diffs are not rendered by default.

Loading
Loading