Update obligations

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-23 14:02:18 +02:00
parent c54051dd8a
commit 98d7e7b7b9
39 changed files with 3746 additions and 394 deletions

View File

@@ -0,0 +1,26 @@
ALTER TYPE obligations_status RENAME VALUE 'OPEN' TO 'NON_COMPLIANT';
ALTER TYPE obligations_status RENAME VALUE 'IN_PROGRESS' TO 'PARTIALLY_COMPLIANT';
ALTER TYPE obligations_status RENAME VALUE 'CLOSED' TO 'COMPLIANT';
ALTER TABLE obligations DROP COLUMN reference_id;
CREATE TABLE risks_obligations (
risk_id TEXT NOT NULL REFERENCES risks(id) ON UPDATE CASCADE ON DELETE CASCADE,
obligation_id TEXT NOT NULL REFERENCES obligations(id) ON UPDATE CASCADE ON DELETE CASCADE,
tenant_id TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (risk_id, obligation_id)
);
ALTER TABLE obligations ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('simple',
COALESCE(requirement, '') || ' ' ||
COALESCE(area, '') || ' ' ||
COALESCE(source, '') || ' ' ||
COALESCE(regulator, '') || ' ' ||
COALESCE(actions_to_be_implemented, '')
)
) STORED;
CREATE INDEX obligations_search_idx ON obligations USING gin(search_vector);

View File

@@ -30,7 +30,6 @@ type (
Obligation struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ReferenceID string `db:"reference_id"`
Area *string `db:"area"`
Source *string `db:"source"`
Requirement *string `db:"requirement"`
@@ -59,8 +58,6 @@ func (o *Obligation) CursorKey(field ObligationOrderField) page.CursorKey {
return page.NewCursorKey(o.ID, o.DueDate)
case ObligationOrderFieldStatus:
return page.NewCursorKey(o.ID, o.Status)
case ObligationOrderFieldReferenceId:
return page.NewCursorKey(o.ID, o.ReferenceID)
}
panic(fmt.Sprintf("unsupported order by: %s", field))
@@ -78,7 +75,6 @@ SELECT
organization_id,
snapshot_id,
source_id,
reference_id,
area,
source,
requirement,
@@ -153,6 +149,52 @@ WHERE
return count, nil
}
func (os *Obligations) CountByRiskID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
riskID gid.GID,
filter *ObligationFilter,
) (int, error) {
q := `
WITH obls AS (
SELECT
o.id,
o.tenant_id,
o.snapshot_id,
o.search_vector
FROM
obligations o
INNER JOIN
risks_obligations ro ON o.id = ro.obligation_id
WHERE
ro.risk_id = @risk_id
)
SELECT
COUNT(id)
FROM
obls
WHERE %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{"risk_id": riskID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot count obligations: %w", err)
}
return count, nil
}
func (os *Obligations) LoadByOrganizationID(
ctx context.Context,
conn pg.Conn,
@@ -165,7 +207,6 @@ func (os *Obligations) LoadByOrganizationID(
SELECT
id,
organization_id,
reference_id,
area,
source,
requirement,
@@ -210,6 +251,86 @@ WHERE
return nil
}
func (os *Obligations) LoadByRiskID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
riskID gid.GID,
cursor *page.Cursor[ObligationOrderField],
filter *ObligationFilter,
) error {
q := `
WITH obls AS (
SELECT
o.id,
o.organization_id,
o.area,
o.source,
o.requirement,
o.actions_to_be_implemented,
o.regulator,
o.owner_id,
o.last_review_date,
o.due_date,
o.status,
o.snapshot_id,
o.source_id,
o.created_at,
o.updated_at,
o.tenant_id,
o.search_vector
FROM
obligations o
INNER JOIN
risks_obligations ro ON o.id = ro.obligation_id
WHERE
ro.risk_id = @risk_id
)
SELECT
id,
organization_id,
area,
source,
requirement,
actions_to_be_implemented,
regulator,
owner_id,
last_review_date,
due_date,
status,
snapshot_id,
source_id,
created_at,
updated_at
FROM
obls
WHERE %s
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"risk_id": riskID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query obligations: %w", err)
}
obligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Obligation])
if err != nil {
return fmt.Errorf("cannot collect obligations: %w", err)
}
*os = obligations
return nil
}
func (o *Obligation) Insert(
ctx context.Context,
conn pg.Conn,
@@ -220,7 +341,6 @@ INSERT INTO obligations (
id,
tenant_id,
organization_id,
reference_id,
area,
source,
requirement,
@@ -238,7 +358,6 @@ INSERT INTO obligations (
@id,
@tenant_id,
@organization_id,
@reference_id,
@area,
@source,
@requirement,
@@ -259,7 +378,6 @@ INSERT INTO obligations (
"id": o.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": o.OrganizationID,
"reference_id": o.ReferenceID,
"area": o.Area,
"source": o.Source,
"requirement": o.Requirement,
@@ -290,7 +408,6 @@ func (o *Obligation) Update(
) error {
q := `
UPDATE obligations SET
reference_id = @reference_id,
area = @area,
source = @source,
requirement = @requirement,
@@ -311,7 +428,6 @@ WHERE
args := pgx.StrictNamedArgs{
"id": o.ID,
"reference_id": o.ReferenceID,
"area": o.Area,
"source": o.Source,
"requirement": o.Requirement,
@@ -367,7 +483,6 @@ INSERT INTO obligations (
snapshot_id,
source_id,
organization_id,
reference_id,
area,
source,
requirement,
@@ -386,7 +501,6 @@ SELECT
@snapshot_id,
o.id,
o.organization_id,
o.reference_id,
o.area,
o.source,
o.requirement,

View File

@@ -25,7 +25,6 @@ const (
ObligationOrderFieldLastReviewDate ObligationOrderField = "LAST_REVIEW_DATE"
ObligationOrderFieldDueDate ObligationOrderField = "DUE_DATE"
ObligationOrderFieldStatus ObligationOrderField = "STATUS"
ObligationOrderFieldReferenceId ObligationOrderField = "REFERENCE_ID"
)
func (p ObligationOrderField) Column() string {
@@ -46,8 +45,7 @@ func (p *ObligationOrderField) UnmarshalText(text []byte) error {
case string(ObligationOrderFieldCreatedAt),
string(ObligationOrderFieldLastReviewDate),
string(ObligationOrderFieldDueDate),
string(ObligationOrderFieldStatus),
string(ObligationOrderFieldReferenceId):
string(ObligationOrderFieldStatus):
*p = ObligationOrderField(val)
return nil
}

View File

@@ -22,9 +22,9 @@ import (
type ObligationStatus string
const (
ObligationStatusOpen ObligationStatus = "OPEN"
ObligationStatusInProgress ObligationStatus = "IN_PROGRESS"
ObligationStatusClosed ObligationStatus = "CLOSED"
ObligationStatusNonCompliant ObligationStatus = "NON_COMPLIANT"
ObligationStatusPartiallyCompliant ObligationStatus = "PARTIALLY_COMPLIANT"
ObligationStatusCompliant ObligationStatus = "COMPLIANT"
)
func (os ObligationStatus) String() string {
@@ -43,12 +43,12 @@ func (os *ObligationStatus) Scan(value any) error {
}
switch s {
case "OPEN":
*os = ObligationStatusOpen
case "IN_PROGRESS":
*os = ObligationStatusInProgress
case "CLOSED":
*os = ObligationStatusClosed
case "NON_COMPLIANT":
*os = ObligationStatusNonCompliant
case "PARTIALLY_COMPLIANT":
*os = ObligationStatusPartiallyCompliant
case "COMPLIANT":
*os = ObligationStatusCompliant
default:
return fmt.Errorf("invalid ObligationStatus value: %q", s)
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2025 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.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
RiskObligation struct {
RiskID gid.GID `db:"risk_id"`
ObligationID gid.GID `db:"obligation_id"`
CreatedAt time.Time `db:"created_at"`
}
RiskObligations []*RiskObligation
)
func (ro RiskObligation) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO risks_obligations (
risk_id,
obligation_id,
tenant_id,
created_at
) VALUES (
@risk_id,
@obligation_id,
@tenant_id,
@created_at
)
`
args := pgx.StrictNamedArgs{
"risk_id": ro.RiskID,
"obligation_id": ro.ObligationID,
"tenant_id": scope.GetTenantID(),
"created_at": ro.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot insert risk obligation: %w", err)
}
return nil
}
func (ro RiskObligation) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE FROM risks_obligations
WHERE
%s
AND risk_id = @risk_id
AND obligation_id = @obligation_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"risk_id": ro.RiskID,
"obligation_id": ro.ObligationID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete risk obligation: %w", err)
}
return nil
}