feat(t-paliad-148) commit 4/6: reminder + deadline + derivation cleanup — pt.role → pt.responsibility
reminder_service.go: BuildDigest audience predicate switches the
"project lead anywhere on the path" branch from `pt.role = 'lead'` to
`pt.responsibility = 'lead'`. Two SQL sites + comment updated.
deadline_service.go: assertCanAdminProject (Reopen permission) switches
from `pt.role IN ('admin','lead')` to `pt.responsibility = 'lead'`.
The legacy 'admin' was already dead since t-paliad-051 — never present
in project_teams.role to begin with — so this also drops a slow leak.
Doc comments + error message updated.
derivation_service.go: ListDescendantStaffed SELECTs both `pt.role` and
`pt.responsibility`, returns the new column to the team-tab "from
descendants" subsection (so the firm-tier badge + responsibility pill
both render). ORDER BY switches to responsibility.
Build + vet clean. Pure-Go tests pass.
This commit is contained in:
@@ -626,7 +626,7 @@ func (s *DeadlineService) Complete(ctx context.Context, userID, deadlineID uuid.
|
||||
|
||||
// Reopen flips a completed Deadline back to pending and clears completed_at.
|
||||
// Authorization: global admin OR a member of the Project (or any ancestor)
|
||||
// with project_teams.role IN ('admin','lead'). Other authenticated viewers
|
||||
// with project_teams.responsibility = 'lead'. Other authenticated viewers
|
||||
// can see the Deadline but cannot reopen it.
|
||||
func (s *DeadlineService) Reopen(ctx context.Context, userID, deadlineID uuid.UUID) (*models.Deadline, error) {
|
||||
current, err := s.GetByID(ctx, userID, deadlineID)
|
||||
@@ -667,11 +667,17 @@ func (s *DeadlineService) Reopen(ctx context.Context, userID, deadlineID uuid.UU
|
||||
|
||||
// assertCanAdminProject returns nil if the user may perform admin-level
|
||||
// actions on the Project (reopen, future bulk ops). Pass-conditions:
|
||||
// - global users.role = 'admin', or
|
||||
// - direct/inherited project_teams membership with role IN ('admin','lead').
|
||||
// - users.global_role = 'global_admin', or
|
||||
// - direct/inherited project_teams membership with responsibility = 'lead'.
|
||||
//
|
||||
// Returns ErrForbidden otherwise. Visibility must be checked separately
|
||||
// (callers do this via GetByID before calling here).
|
||||
//
|
||||
// t-paliad-148: switched from `role IN ('admin','lead')` to
|
||||
// `responsibility = 'lead'`. The legacy 'admin' value was already dead
|
||||
// since t-paliad-051 (project_teams.role never had an 'admin' value;
|
||||
// only the legacy users.role enum did, before it was split into
|
||||
// global_role).
|
||||
func (s *DeadlineService) assertCanAdminProject(ctx context.Context, userID, projectID uuid.UUID) error {
|
||||
user, err := s.users().GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -692,13 +698,13 @@ func (s *DeadlineService) assertCanAdminProject(ctx context.Context, userID, pro
|
||||
ON pt.project_id = ANY(string_to_array(p.path, '.')::uuid[])
|
||||
WHERE p.id = $1
|
||||
AND pt.user_id = $2
|
||||
AND pt.role IN ('admin', 'lead')
|
||||
AND pt.responsibility = 'lead'
|
||||
)`, projectID, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check project admin: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: only project admins/leads can reopen Deadlines", ErrForbidden)
|
||||
return fmt.Errorf("%w: only project leads can reopen Deadlines", ErrForbidden)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -316,7 +316,8 @@ func (s *DerivationService) ListDescendantStaffed(ctx context.Context, callerID,
|
||||
WHERE target.id = $1
|
||||
),
|
||||
descendant_rows AS (
|
||||
SELECT pt.id, pt.project_id, pt.user_id, pt.role, pt.added_by, pt.created_at,
|
||||
SELECT pt.id, pt.project_id, pt.user_id, pt.role, pt.responsibility,
|
||||
pt.added_by, pt.created_at,
|
||||
d.title AS source_title
|
||||
FROM paliad.project_teams pt
|
||||
JOIN descendants d ON d.id = pt.project_id
|
||||
@@ -333,18 +334,19 @@ func (s *DerivationService) ListDescendantStaffed(ctx context.Context, callerID,
|
||||
) AS rn
|
||||
FROM descendant_rows dr
|
||||
)
|
||||
SELECT d.id, d.project_id, d.user_id, d.role,
|
||||
SELECT d.id, d.project_id, d.user_id, d.role, d.responsibility,
|
||||
true AS inherited,
|
||||
d.added_by, d.created_at,
|
||||
u.email AS user_email,
|
||||
u.display_name AS user_display_name,
|
||||
u.office AS user_office,
|
||||
u.profession AS user_profession,
|
||||
d.project_id AS inherited_from_id,
|
||||
d.source_title AS inherited_from_title
|
||||
FROM dedup d
|
||||
JOIN paliad.users u ON u.id = d.user_id
|
||||
WHERE d.rn = 1
|
||||
ORDER BY d.role, u.display_name`,
|
||||
ORDER BY d.responsibility, u.display_name`,
|
||||
projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list descendant-staffed: %w", err)
|
||||
|
||||
@@ -296,7 +296,7 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
|
||||
// Audience predicates:
|
||||
// * owner of the deadline — f.created_by = U
|
||||
// * project lead anywhere on the path — pt.role = 'lead'
|
||||
// * project lead anywhere on the path — pt.responsibility = 'lead'
|
||||
// * owner's escalation contact (override) — own.escalation_contact_id = U
|
||||
// * global admin AND owner has no override — fallback channel
|
||||
// Per-category recipient rules (e.g. leads don't get overdue) are applied
|
||||
@@ -314,7 +314,7 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
EXISTS (
|
||||
SELECT 1 FROM paliad.project_teams pt
|
||||
WHERE pt.user_id = $2
|
||||
AND pt.role = 'lead'
|
||||
AND pt.responsibility = 'lead'
|
||||
AND pt.project_id = ANY(string_to_array(p.path, '.')::uuid[])
|
||||
) AS is_lead
|
||||
FROM paliad.deadlines f
|
||||
@@ -327,7 +327,7 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM paliad.project_teams pt
|
||||
WHERE pt.user_id = $2
|
||||
AND pt.role = 'lead'
|
||||
AND pt.responsibility = 'lead'
|
||||
AND pt.project_id = ANY(string_to_array(p.path, '.')::uuid[])
|
||||
)
|
||||
OR own.escalation_contact_id = $2
|
||||
|
||||
Reference in New Issue
Block a user