-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclose_election_by_owner.go
149 lines (118 loc) · 3.86 KB
/
close_election_by_owner.go
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 election
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/inklabs/cqrs"
"github.com/inklabs/cqrs/pkg/clock"
"github.com/inklabs/vote/event"
"github.com/inklabs/vote/internal/authorization"
"github.com/inklabs/vote/internal/electionrepository"
"github.com/inklabs/vote/internal/rcv"
"github.com/inklabs/vote/pkg/sleep"
)
// CloseElectionByOwner is an asynchronous command that closes an election and
// calculates a winner by using the Ranked Choice Voting (RCV) electoral system.
type CloseElectionByOwner struct {
ID string
ElectionID string
}
type closeElectionByOwnerHandler struct {
repository electionrepository.Repository
clock clock.Clock
}
func NewCloseElectionByOwnerHandler(
repository electionrepository.Repository,
clock clock.Clock,
) *closeElectionByOwnerHandler {
return &closeElectionByOwnerHandler{
repository: repository,
clock: clock,
}
}
func (h *closeElectionByOwnerHandler) Verify(ctx authorization.Context, cmd CloseElectionByOwner) error {
election, err := h.repository.GetElection(ctx.Context(), cmd.ElectionID)
if err != nil {
return err
}
if ctx.UserID() != election.OrganizerUserID {
log.Printf("user %s does not match election organizer user %s", ctx.UserID(), election.OrganizerUserID)
return cqrs.ErrAccessDenied
}
return nil
}
func (h *closeElectionByOwnerHandler) On(ctx context.Context, cmd CloseElectionByOwner, eventRaiser cqrs.EventRaiser, logger cqrs.AsyncCommandLogger) error {
ctx, span := tracer.Start(ctx, "vote.close-election-by-owner")
defer span.End()
election, err := h.repository.GetElection(ctx, cmd.ElectionID)
if err != nil {
logger.LogError("election not found: %s", cmd.ElectionID)
return err
}
winningProposalID, err := h.getWinningProposalID(ctx, cmd.ElectionID, logger)
if err != nil {
logger.LogError("unable to get winning proposal")
err = fmt.Errorf("unable to get winning proposal: %w", err)
cqrs.RecordSpanError(span, err)
return err
}
selectedAt := int(h.clock.Now().Unix())
election.IsClosed = true
election.ClosedAt = selectedAt
election.SelectedAt = selectedAt
election.WinningProposalID = winningProposalID
err = h.repository.SaveElection(ctx, election)
if err != nil {
return err
}
logger.LogInfo("Closing election with winner: %s", winningProposalID)
eventRaiser.Raise(event.ElectionWinnerWasSelected{
ElectionID: cmd.ElectionID,
WinningProposalID: winningProposalID,
SelectedAt: selectedAt,
})
return nil
}
func simulateProcessing(logger cqrs.AsyncCommandLogger, totalToProcess int) {
logger.SetTotalToProcess(totalToProcess)
sleepDuration := 5 * time.Second / time.Duration(totalToProcess)
for i := 0; i < totalToProcess; i++ {
logger.IncrementTotalProcessed()
if totalToProcess < 10 || i%(totalToProcess/10) == 0 {
logger.Flush()
sleep.Sleep(sleepDuration)
}
}
logger.Flush()
}
func (h *closeElectionByOwnerHandler) getWinningProposalID(ctx context.Context, electionID string, logger cqrs.AsyncCommandLogger) (string, error) {
votes, err := h.repository.GetVotes(ctx, electionID)
if err != nil {
return "", err
}
if len(votes) == 0 {
logger.LogError("no votes found for election")
return "", ErrNoVotesFound
}
simulateProcessing(logger, len(votes))
tabulator := rcv.NewSingleWinner(toRankedProposalVotes(votes))
winningProposalID, err := tabulator.GetWinningProposal()
if err != nil {
if errors.Is(err, rcv.ErrWinnerNotFound) {
logger.LogError("winner not found")
}
return "", err
}
return winningProposalID, nil
}
func toRankedProposalVotes(votes []electionrepository.Vote) rcv.Ballots {
var rankedProposalVotes rcv.Ballots
for _, vote := range votes {
proposalIDs := append([]string{}, vote.RankedProposalIDs...)
rankedProposalVotes = append(rankedProposalVotes, proposalIDs)
}
return rankedProposalVotes
}
var ErrNoVotesFound = errors.New("no votes found for election")