Scope document signatures to the major version

A signature applies to a whole major: minor publishes keep it and the
export unions signatures across every minor of the major. The request
guard was scoped to a single minor, so re-requesting on a newer minor
(or twice on the same version) inserted duplicate rows and a signatory
appeared several times on the exported signature page.

Deduplicate by loading any existing signature across the major before
inserting, cancel still-pending requests from prior majors when a new
major is published, and restrict the export to active signatories
(comparing contract end dates against the current date). A migration
collapses the duplicate rows already in the table, preferring a signed
row over a pending one and then the most recent.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-05-28 18:12:32 -07:00
parent 4efba328a6
commit 1a8ca29264
7 changed files with 489 additions and 62 deletions

View File

@@ -151,6 +151,86 @@ LIMIT 1
return nil
}
// LoadByDocumentMajorAndSignatory loads the signatory's existing signature for
// the whole major that owns documentVersionID, scanning across every minor
// version of that major. A signed signature is preferred over a still pending
// one, then the most recent. It returns ErrResourceNotFound when the signatory
// has no signature anywhere in the major.
func (pvs *DocumentVersionSignature) LoadByDocumentMajorAndSignatory(
ctx context.Context,
conn pg.Querier,
scope Scoper,
documentVersionID gid.GID,
signatory gid.GID,
) error {
q := `
WITH source_version AS (
SELECT document_id, major FROM document_versions WHERE id = @document_version_id
),
major_versions AS (
SELECT dv.id FROM document_versions dv
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
),
major_signatures AS (
SELECT
dvs.id,
dvs.organization_id,
dvs.tenant_id,
dvs.document_version_id,
dvs.state,
dvs.signed_by_profile_id,
dvs.signed_at,
dvs.requested_at,
dvs.created_at,
dvs.updated_at
FROM document_version_signatures dvs
INNER JOIN major_versions mv ON dvs.document_version_id = mv.id
WHERE dvs.signed_by_profile_id = @signatory
)
SELECT
id,
organization_id,
document_version_id,
state,
signed_by_profile_id,
signed_at,
requested_at,
created_at,
updated_at
FROM
major_signatures
WHERE
%s
ORDER BY
CASE state WHEN 'SIGNED' THEN 0 ELSE 1 END,
created_at DESC
LIMIT 1
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"document_version_id": documentVersionID, "signatory": signatory}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query document version signature by major: %w", err)
}
documentVersionSignature, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[DocumentVersionSignature])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect document version signature by major: %w", err)
}
*pvs = documentVersionSignature
return nil
}
func (pvs *DocumentVersionSignature) LoadByID(
ctx context.Context,
conn pg.Querier,
@@ -402,6 +482,42 @@ WHERE
return nil
}
func (pvss *DocumentVersionSignatures) DeleteRequestedByDocumentIDBelowMajor(
ctx context.Context,
conn pg.Tx,
scope Scoper,
documentID gid.GID,
major int,
) error {
q := `
DELETE FROM document_version_signatures
WHERE
%s
AND state = @state
AND document_version_id IN (
SELECT id
FROM document_versions
WHERE document_id = @document_id
AND major < @major
)
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"document_id": documentID,
"major": major,
"state": DocumentVersionSignatureStateRequested,
}
maps.Copy(args, scope.SQLArguments())
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot delete requested document version signatures from previous major versions: %w", err)
}
return nil
}
func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeople(
ctx context.Context,
conn pg.Querier,
@@ -433,6 +549,18 @@ signatures_with_people AS (
FROM document_version_signatures dvs
INNER JOIN major_versions mv ON dvs.document_version_id = mv.id
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
WHERE
dvs.state = 'SIGNED'
OR (
p.state = 'ACTIVE'
AND (p.contract_end_date IS NULL OR p.contract_end_date >= CURRENT_DATE)
AND EXISTS (
SELECT 1
FROM iam_memberships m
WHERE m.identity_id = p.identity_id
AND m.organization_id = p.organization_id
)
)
)
SELECT
id,

View File

@@ -62,12 +62,12 @@ func (f *DocumentVersionSignatureFilter) SQLFragment() string {
(
@active_contract::boolean = TRUE
AND (
p.contract_end_date IS NULL OR p.contract_end_date > NOW()
p.contract_end_date IS NULL OR p.contract_end_date >= CURRENT_DATE
)
) OR (
@active_contract::boolean = FALSE
AND (
p.contract_end_date IS NOT NULL AND p.contract_end_date <= NOW()
p.contract_end_date IS NOT NULL AND p.contract_end_date < CURRENT_DATE
)
)
)

View File

@@ -0,0 +1,39 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-- PERFORMANCE OF THIS SOFTWARE.
-- A signature applies to a whole major version: minor publishes keep it and the
-- export unions signatures across every minor of the major. A signatory must
-- therefore have at most one signature per (document, major). Historically the
-- request guard was scoped to a single minor version, so re-requesting on a
-- newer minor (or twice on the same version) inserted duplicate rows, making a
-- person appear several times on the exported signature page.
--
-- Collapse each (document, major, signatory) group to a single signature,
-- keeping the same row the application now prefers: a SIGNED signature over a
-- pending one, then the most recently created.
WITH ranked AS (
SELECT
dvs.id,
ROW_NUMBER() OVER (
PARTITION BY dv.document_id, dv.major, dvs.signed_by_profile_id
ORDER BY
CASE dvs.state WHEN 'SIGNED' THEN 0 ELSE 1 END,
dvs.created_at DESC,
dvs.id DESC
) AS rn
FROM document_version_signatures dvs
INNER JOIN document_versions dv ON dvs.document_version_id = dv.id
)
DELETE FROM document_version_signatures
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);