SOA as document: replace export with publish workflow

Statements of Applicability are no longer exported as one-off PDFs.
Instead, each SOA owns a persistent document that accumulates versions
over time, following the same publish/approve lifecycle as authored
documents.

Publishing without approvers publishes immediately; publishing with
approvers creates a draft pending approval via the existing quorum
system. SOAs can also store default approvers that are pre-populated in
the publish dialog.

The SOA is removed from the snapshot system — applicability statements
are now queried directly (snapshot_id IS NULL) rather than through
snapshot copies.

A standalone migration script (cmd/migrate-soa-snapshots-to-documents)
converts existing SOA snapshots into documents with proper ProseMirror
content, preserving version history and approval decisions.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-10 13:38:42 +02:00
parent 53edc5ba26
commit c635492f75
74 changed files with 3509 additions and 1595 deletions

View File

@@ -33,6 +33,11 @@ type (
}
ControlObligations []*ControlObligation
ControlObligationType struct {
ControlID gid.GID `db:"control_id"`
ObligationType ObligationType `db:"obligation_type"`
}
)
func (co ControlObligation) Upsert(
@@ -137,3 +142,48 @@ WHERE %s
return count, nil
}
func LoadObligationTypesByControlIDs(
ctx context.Context,
conn pg.Querier,
scope Scoper,
controlIDs []gid.GID,
) ([]ControlObligationType, error) {
q := `
WITH control_obls AS (
SELECT DISTINCT
co.control_id,
o.type AS obligation_type,
o.tenant_id
FROM
controls_obligations co
INNER JOIN
obligations o ON co.obligation_id = o.id
WHERE
co.control_id = ANY(@control_ids)
)
SELECT
control_id,
obligation_type
FROM
control_obls
WHERE
%s;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"control_ids": controlIDs}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return nil, fmt.Errorf("cannot load obligation types by control IDs: %w", err)
}
result, err := pgx.CollectRows(rows, pgx.RowToStructByName[ControlObligationType])
if err != nil {
return nil, fmt.Errorf("cannot collect control obligation types: %w", err)
}
return result, nil
}