reconcile and rep ids sorted
This commit is contained in:
parent
ede7884b31
commit
e0911df455
5 changed files with 112 additions and 12 deletions
58
client.go
58
client.go
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
"bytes"
|
||||||
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
@ -123,7 +124,7 @@ func departmentOf(props map[string]json.RawMessage) string {
|
||||||
|
|
||||||
//optionLabels returne the labels of an option-valued custom-property
|
//optionLabels returne the labels of an option-valued custom-property
|
||||||
// ST serialises select and checkbox fields as [{label, value}], and the
|
// ST serialises select and checkbox fields as [{label, value}], and the
|
||||||
// same label can appear more than once.
|
// same label appears more than one time
|
||||||
|
|
||||||
func optionLabels(props map[string]json.RawMessage, key string) []string {
|
func optionLabels(props map[string]json.RawMessage, key string) []string {
|
||||||
raw, ok := props[key]
|
raw, ok := props[key]
|
||||||
|
|
@ -163,6 +164,61 @@ func (c *Client) GetUser(ctx context.Context, id int64) (*User, error) {
|
||||||
return u.toUser(), nil
|
return u.toUser(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveAgent returns the user ID of the person's current agent, or 0 if
|
||||||
|
// they have none. The is_active query param is ignored by the API, so
|
||||||
|
// rows are filtered here.
|
||||||
|
func (c *Client) ActiveAgent(ctx context.Context, userID int64) (int64, error) {
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("user_id", strconv.FormatInt(userID, 10))
|
||||||
|
q.Set("_limit", "100")
|
||||||
|
|
||||||
|
raw, err := c.do(ctx, http.MethodGet, "/agent_assignments?"+q.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var env struct {
|
||||||
|
Data []struct {
|
||||||
|
AgentUserID int64 `json:"agent_user_id"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &env); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range env.Data {
|
||||||
|
if a.IsActive {
|
||||||
|
return a.AgentUserID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignAgent creates an agent assignment. ST deactivates any prior active
|
||||||
|
// assignment automatically. Not idempotent: a repeat call creates a new row.
|
||||||
|
func (c *Client) AssignAgent(ctx context.Context, userID, agentID int64) error {
|
||||||
|
if c.dryRun {
|
||||||
|
log.Printf("DRY RUN: would assign user %d to agent %d", userID, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(map[string]int64{
|
||||||
|
"user_id": userID,
|
||||||
|
"agent_user_id": agentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.do(ctx, http.MethodPost, "/agent_assignments", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("assigned user %d to agent %d", userID, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListUsers pages the full user list. If since is non-zero, only users
|
// ListUsers pages the full user list. If since is non-zero, only users
|
||||||
// updated after that Unix timestamp are returned.
|
// updated after that Unix timestamp are returned.
|
||||||
func (c *Client) ListUsers(ctx context.Context, since int64) ([]User, error) {
|
func (c *Client) ListUsers(ctx context.Context, since int64) ([]User, error) {
|
||||||
|
|
|
||||||
17
fakes.go
17
fakes.go
|
|
@ -6,8 +6,23 @@ import (
|
||||||
|
|
||||||
type fakeST struct {
|
type fakeST struct {
|
||||||
dept string
|
dept string
|
||||||
err error
|
|
||||||
chapterID int64
|
chapterID int64
|
||||||
|
err error
|
||||||
|
|
||||||
|
agents map[int64]int64 // userID → existing agent, 0 or absent = none
|
||||||
|
assigned map[int64]int64 // recorded writes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeST) ActiveAgent(ctx context.Context, userID int64) (int64, error) {
|
||||||
|
return f.agents[userID], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeST) AssignAgent(ctx context.Context, userID, agentID int64) error {
|
||||||
|
if f.assigned == nil {
|
||||||
|
f.assigned = map[int64]int64{}
|
||||||
|
}
|
||||||
|
f.assigned[userID] = agentID
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeST) GetUser(ctx context.Context, id int64) (*User, error) {
|
func (f *fakeST) GetUser(ctx context.Context, id int64) (*User, error) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func (s *Server) reconcile(ctx context.Context, runID int64, rep *User, dept str
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop
|
// loop
|
||||||
assigned, skipped, failed := 0, 0, 0
|
assigned, skipped, failed := 0, 0, 0
|
||||||
|
|
||||||
for _, m := range members {
|
for _, m := range members {
|
||||||
|
|
@ -38,7 +38,7 @@ func (s *Server) reconcile(ctx context.Context, runID int64, rep *User, dept str
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, err := client.ActiveAgent(ctx, m)
|
existing, err := s.st.ActiveAgent(ctx, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("run %d: checking agent for %d failed: %v", runID, m, err)
|
log.Printf("run %d: checking agent for %d failed: %v", runID, m, err)
|
||||||
failed++
|
failed++
|
||||||
|
|
@ -54,7 +54,7 @@ func (s *Server) reconcile(ctx context.Context, runID int64, rep *User, dept str
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.AssignAgent(ctx, m, agent); err != nil {
|
if err := s.st.AssignAgent(ctx, m, agent); err != nil {
|
||||||
log.Printf("run %d: assigning %d to %d failed: %v", runID, m, agent, err)
|
log.Printf("run %d: assigning %d to %d failed: %v", runID, m, agent, err)
|
||||||
failed++
|
failed++
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ type User struct {
|
||||||
// STClient is what the handler needs from the Solidarity Tech API.
|
// STClient is what the handler needs from the Solidarity Tech API.
|
||||||
type STClient interface {
|
type STClient interface {
|
||||||
GetUser(ctx context.Context, id int64) (*User, error)
|
GetUser(ctx context.Context, id int64) (*User, error)
|
||||||
|
ActiveAgent(ctx context.Context, userID int64) (int64, error)
|
||||||
|
AssignAgent(ctx context.Context, userID, agentID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store is what the handler needs from persistence.
|
// Store is what the handler needs from persistence.
|
||||||
|
|
@ -37,6 +39,8 @@ type Store interface {
|
||||||
SetLastSync(ts int64) error
|
SetLastSync(ts int64) error
|
||||||
RepsFor(chapterID int64, dept string) ([]int64, error)
|
RepsFor(chapterID int64, dept string) ([]int64, error)
|
||||||
EligibleMembers(chapterID int64, dept string) ([]int64, error)
|
EligibleMembers(chapterID int64, dept string) ([]int64, error)
|
||||||
|
LogSkip(runID, userID, existingAgent, wouldHaveAssigned int64) error
|
||||||
|
SetProgress(runID, lastUserID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrLocked is returned by AcquireLock when a run is already in flight
|
// ErrLocked is returned by AcquireLock when a run is already in flight
|
||||||
|
|
|
||||||
25
store.go
25
store.go
|
|
@ -18,9 +18,18 @@ var schemaStatements = []string{
|
||||||
department TEXT NOT NULL,
|
department TEXT NOT NULL,
|
||||||
rep_user_id INTEGER NOT NULL,
|
rep_user_id INTEGER NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
|
last_user_id INTEGER,
|
||||||
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
ended_at TEXT
|
ended_at TEXT
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS skips (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
run_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
existing_agent INTEGER NOT NULL,
|
||||||
|
would_have_assigned INTEGER NOT NULL,
|
||||||
|
at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
`CREATE UNIQUE INDEX IF NOT EXISTS one_run_per_dept
|
`CREATE UNIQUE INDEX IF NOT EXISTS one_run_per_dept
|
||||||
ON runs(chapter_id, department) WHERE status = 'running'`,
|
ON runs(chapter_id, department) WHERE status = 'running'`,
|
||||||
`CREATE TABLE IF NOT EXISTS people (
|
`CREATE TABLE IF NOT EXISTS people (
|
||||||
|
|
@ -179,3 +188,19 @@ func (s *SQLStore) AcquireLock(chapterID int64, dept string, repID int64) (int64
|
||||||
strconv.FormatInt(ts, 10))
|
strconv.FormatInt(ts, 10))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogSkip records a member left alone because they already had an agent.
|
||||||
|
// This is the manual-fix queue: nothing else records what fill-only cost.
|
||||||
|
func (s *SQLStore) LogSkip(runID, userID, existingAgent, wouldHaveAssigned int64) error {
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
`INSERT INTO skips (run_id, user_id, existing_agent, would_have_assigned)
|
||||||
|
VALUES (?, ?, ?, ?)`,
|
||||||
|
runID, userID, existingAgent, wouldHaveAssigned)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) SetProgress(runID, lastUserID int64) error {
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
`UPDATE runs SET last_user_id = ? WHERE id = ?`, lastUserID, runID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue