lead claims: outreach v1, decided by Mike
Who picked a lead up and when, so nothing sits waiting unnoticed. No outcomes, no end states, nothing on join_leads changes: the lead stays the record of what the family said and the claim is only who has it. One row per claim in lead_claims; the current claim is the latest unreleased. Taking over is release then claim, never a silent overwrite: 409 names the holder. The holder or an owner may release. Every claim and release lands in the action log; the summary carries an unclaimed count. tests/smoke_admin.py 112 -> 122.
This commit is contained in:
@@ -74,6 +74,22 @@ CREATE TABLE IF NOT EXISTS join_leads (
|
||||
CREATE INDEX IF NOT EXISTS idx_join_leads_submitted ON join_leads(submitted_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_join_leads_email ON join_leads(email);
|
||||
|
||||
-- Lead claims (outreach v1, decided by Mike 2026-09-04): who picked a lead
|
||||
-- up and when, so nothing sits waiting unnoticed. No outcomes, no end
|
||||
-- states - the lead stays the record of what the family said, this is only
|
||||
-- who has it. One row per claim; the current claim is the latest with no
|
||||
-- released_at. Nothing here touches join_leads.
|
||||
CREATE TABLE IF NOT EXISTS lead_claims (
|
||||
id TEXT PRIMARY KEY,
|
||||
lead_id TEXT NOT NULL REFERENCES join_leads(id),
|
||||
person_id TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
claimed_at TEXT NOT NULL,
|
||||
released_at TEXT,
|
||||
released_by TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_claims_lead ON lead_claims(lead_id, released_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mirrors (
|
||||
record_id TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
@@ -272,6 +288,11 @@ def get_lead(record_id):
|
||||
out["mirrors"] = [dict(m) for m in con.execute(
|
||||
"SELECT target, state, attempts, last_error, last_attempt_at"
|
||||
" FROM mirrors WHERE record_id=?", (record_id,))]
|
||||
cur = current_claim(con, record_id)
|
||||
out["claim"] = {"email": cur["email"], "claimed_at": cur["claimed_at"]} if cur else None
|
||||
out["claim_history"] = [dict(c) for c in con.execute(
|
||||
"SELECT email, claimed_at, released_at, released_by FROM lead_claims WHERE lead_id=?"
|
||||
" ORDER BY claimed_at DESC", (record_id,))]
|
||||
return out
|
||||
finally:
|
||||
con.close()
|
||||
@@ -302,6 +323,13 @@ def list_leads(since=None, q=None, limit=100, offset=0):
|
||||
mir.setdefault(m["record_id"], {})[m["target"]] = m["state"]
|
||||
for r in rows:
|
||||
r["mirrors"] = mir.get(r["id"], {})
|
||||
claims = {}
|
||||
for c in con.execute(
|
||||
"SELECT lead_id, email, claimed_at FROM lead_claims WHERE released_at IS NULL"
|
||||
" AND lead_id IN (%s)" % marks, ids):
|
||||
claims[c["lead_id"]] = {"email": c["email"], "claimed_at": c["claimed_at"]}
|
||||
for r in rows:
|
||||
r["claim"] = claims.get(r["id"])
|
||||
return rows
|
||||
finally:
|
||||
con.close()
|
||||
@@ -313,6 +341,8 @@ def summary():
|
||||
try:
|
||||
return {
|
||||
"total": con.execute("SELECT COUNT(*) c FROM join_leads").fetchone()["c"],
|
||||
"unclaimed": con.execute("SELECT count(*) FROM join_leads l WHERE NOT EXISTS"
|
||||
" (SELECT 1 FROM lead_claims c WHERE c.lead_id=l.id AND c.released_at IS NULL)").fetchone()[0],
|
||||
"by_interest": {r["interested_in"]: r["c"] for r in con.execute(
|
||||
"SELECT interested_in, COUNT(*) c FROM join_leads GROUP BY interested_in")},
|
||||
"by_heard_from": {r["heard_from"]: r["c"] for r in con.execute(
|
||||
@@ -567,6 +597,69 @@ def revoke_announcement(aid):
|
||||
# Rows are deactivated, never deleted. A unit that folds or moves keeps its
|
||||
# row with active=0, so "why did that pack disappear from the page" stays
|
||||
# answerable. Reactivation is an update setting active back to 1.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lead claims - outreach v1. See the schema comment.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ClaimRejected(Rejected):
|
||||
pass
|
||||
|
||||
|
||||
def current_claim(con, lead_id):
|
||||
r = con.execute("SELECT * FROM lead_claims WHERE lead_id=? AND released_at IS NULL"
|
||||
" ORDER BY claimed_at DESC LIMIT 1", (lead_id,)).fetchone()
|
||||
return dict(r) if r else None
|
||||
|
||||
|
||||
def claim_lead(lead_id, person_id, email):
|
||||
"""Take a lead. If someone else holds it, 409 with who - taking it over
|
||||
is a deliberate release-then-claim, never a silent overwrite."""
|
||||
con = connect()
|
||||
try:
|
||||
if not con.execute("SELECT 1 FROM join_leads WHERE id=?", (lead_id,)).fetchone():
|
||||
return None
|
||||
cur = current_claim(con, lead_id)
|
||||
if cur:
|
||||
if cur["person_id"] == person_id:
|
||||
raise ClaimRejected(409, "you already have this lead")
|
||||
raise ClaimRejected(409, "%s has this lead; release it first" % cur["email"], {"claimed_by": cur["email"]})
|
||||
con.execute("INSERT INTO lead_claims (id, lead_id, person_id, email, claimed_at) VALUES (?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), lead_id, person_id, email, _now()))
|
||||
con.commit()
|
||||
return current_claim(con, lead_id)
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def release_lead(lead_id, person_id, email, can_release_any=False):
|
||||
"""Let a lead go. The holder may; so may someone with people:manage
|
||||
(an owner reassigning), which is what can_release_any means."""
|
||||
con = connect()
|
||||
try:
|
||||
if not con.execute("SELECT 1 FROM join_leads WHERE id=?", (lead_id,)).fetchone():
|
||||
return None
|
||||
cur = current_claim(con, lead_id)
|
||||
if not cur:
|
||||
raise ClaimRejected(409, "nobody has this lead")
|
||||
if cur["person_id"] != person_id and not can_release_any:
|
||||
raise ClaimRejected(403, "%s has this lead; only they or an owner can release it" % cur["email"])
|
||||
con.execute("UPDATE lead_claims SET released_at=?, released_by=? WHERE id=?", (_now(), email, cur["id"]))
|
||||
con.commit()
|
||||
return cur
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def unclaimed_count():
|
||||
con = connect()
|
||||
try:
|
||||
return con.execute("SELECT count(*) FROM join_leads l WHERE NOT EXISTS"
|
||||
" (SELECT 1 FROM lead_claims c WHERE c.lead_id=l.id AND c.released_at IS NULL)").fetchone()[0]
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user