feat: Werkzeugkatalog als Tabelle, Aufbewahrung nullable, kontrolliertes Zweck-Vokabular, Subprozessoren
Katalog (zentral + mandantenseitig) zeigt jetzt eine mehrspaltige Tabelle statt einer Liste. aufbewahrung_tage ist nullable (NULL = vom Anbieter nicht beziffert, unterscheidbar von echter 0-Tage- Zusicherung). geeignete_zwecke ist Checkbox-Auswahl aus den sechs Fragebogen-Zweck-Kategorien statt Freitext. Neues Feld subprozessoren macht Unterauftragsverarbeiter durchsuchbar statt nur Freitext in Einschränkungen. 180-Tage-Frische-Markierung in beiden Katalogansichten ergänzt.
This commit is contained in:
5
internal/store/migrations/0015_werkzeug_details.down.sql
Normal file
5
internal/store/migrations/0015_werkzeug_details.down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE werkzeug DROP COLUMN subprozessoren;
|
||||
|
||||
UPDATE werkzeug SET aufbewahrung_tage = 0 WHERE aufbewahrung_tage IS NULL;
|
||||
ALTER TABLE werkzeug ALTER COLUMN aufbewahrung_tage SET DEFAULT 0;
|
||||
ALTER TABLE werkzeug ALTER COLUMN aufbewahrung_tage SET NOT NULL;
|
||||
12
internal/store/migrations/0015_werkzeug_details.up.sql
Normal file
12
internal/store/migrations/0015_werkzeug_details.up.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- aufbewahrung_tage war bisher NOT NULL DEFAULT 0 — das machte "0" doppeldeutig:
|
||||
-- mal eine echte, belegte Zusicherung (z. B. DeepL: sofortige Löschung),
|
||||
-- mal ein Platzhalter für "vom Anbieter nicht beziffert". NULL trennt das
|
||||
-- sauber: "unbekannt" statt "angeblich sofort gelöscht".
|
||||
ALTER TABLE werkzeug ALTER COLUMN aufbewahrung_tage DROP NOT NULL;
|
||||
ALTER TABLE werkzeug ALTER COLUMN aufbewahrung_tage DROP DEFAULT;
|
||||
|
||||
-- Subprozessoren (z. B. Anthropic als Unterauftragsverarbeiter bei
|
||||
-- Microsoft 365 Copilot, AWS bei DeepL) verändern die EU/USA-Einstufung
|
||||
-- eines Werkzeugs materiell — bisher nur als Freitext in
|
||||
-- "einschraenkungen" erfasst, jetzt als eigenes, durchsuchbares Feld.
|
||||
ALTER TABLE werkzeug ADD COLUMN subprozessoren TEXT[] NOT NULL DEFAULT '{}';
|
||||
@@ -25,8 +25,9 @@ type Werkzeug struct {
|
||||
AVVURL string
|
||||
TrainingOptOut bool
|
||||
TrainingStandard bool
|
||||
AufbewahrungTage int
|
||||
AufbewahrungTage *int // nil = vom Anbieter nicht beziffert, NICHT gleichbedeutend mit 0 Tagen
|
||||
Zertifizierungen []string
|
||||
Subprozessoren []string
|
||||
GeeigneteZwecke []string
|
||||
Einschraenkungen []string
|
||||
LetztePruefung time.Time
|
||||
@@ -36,7 +37,7 @@ type Werkzeug struct {
|
||||
}
|
||||
|
||||
// WerkzeugInput bündelt die Felder eines Werkzeug-Eintrags für
|
||||
// Create/Update — bei 14 Feldern lesbarer als eine positionale
|
||||
// Create/Update — bei 15 Feldern lesbarer als eine positionale
|
||||
// Parameterliste.
|
||||
type WerkzeugInput struct {
|
||||
AccountID *string
|
||||
@@ -47,8 +48,9 @@ type WerkzeugInput struct {
|
||||
AVVURL string
|
||||
TrainingOptOut bool
|
||||
TrainingStandard bool
|
||||
AufbewahrungTage int
|
||||
AufbewahrungTage *int
|
||||
Zertifizierungen []string
|
||||
Subprozessoren []string
|
||||
GeeigneteZwecke []string
|
||||
Einschraenkungen []string
|
||||
LetztePruefung time.Time
|
||||
@@ -56,8 +58,8 @@ type WerkzeugInput struct {
|
||||
}
|
||||
|
||||
const werkzeugColumns = `id, account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
|
||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, geeignete_zwecke,
|
||||
einschraenkungen, letzte_pruefung, quelle, created_at, updated_at`
|
||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
||||
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle, created_at, updated_at`
|
||||
|
||||
func scanWerkzeug(row interface {
|
||||
Scan(dest ...any) error
|
||||
@@ -65,8 +67,8 @@ func scanWerkzeug(row interface {
|
||||
var w Werkzeug
|
||||
err := row.Scan(
|
||||
&w.ID, &w.AccountID, &w.Name, &w.Anbieter, &w.Verarbeitungsort, &w.AVVVerfuegbar, &w.AVVURL,
|
||||
&w.TrainingOptOut, &w.TrainingStandard, &w.AufbewahrungTage, &w.Zertifizierungen, &w.GeeigneteZwecke,
|
||||
&w.Einschraenkungen, &w.LetztePruefung, &w.Quelle, &w.CreatedAt, &w.UpdatedAt,
|
||||
&w.TrainingOptOut, &w.TrainingStandard, &w.AufbewahrungTage, &w.Zertifizierungen, &w.Subprozessoren,
|
||||
&w.GeeigneteZwecke, &w.Einschraenkungen, &w.LetztePruefung, &w.Quelle, &w.CreatedAt, &w.UpdatedAt,
|
||||
)
|
||||
return w, err
|
||||
}
|
||||
@@ -86,6 +88,9 @@ func normalizeWerkzeugSlices(in *WerkzeugInput) {
|
||||
if in.Einschraenkungen == nil {
|
||||
in.Einschraenkungen = []string{}
|
||||
}
|
||||
if in.Subprozessoren == nil {
|
||||
in.Subprozessoren = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWerkzeug legt einen Katalogeintrag an.
|
||||
@@ -94,12 +99,12 @@ func (s *Store) CreateWerkzeug(ctx context.Context, in WerkzeugInput) (Werkzeug,
|
||||
row := s.Pool.QueryRow(ctx, `
|
||||
INSERT INTO werkzeug (
|
||||
account_id, name, anbieter, verarbeitungsort, avv_verfuegbar, avv_url,
|
||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen,
|
||||
training_opt_out, training_standard, aufbewahrung_tage, zertifizierungen, subprozessoren,
|
||||
geeignete_zwecke, einschraenkungen, letzte_pruefung, quelle
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING `+werkzeugColumns,
|
||||
in.AccountID, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
|
||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen,
|
||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
||||
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
||||
)
|
||||
w, err := scanWerkzeug(row)
|
||||
@@ -120,12 +125,12 @@ func (s *Store) UpdateWerkzeug(ctx context.Context, id string, in WerkzeugInput)
|
||||
UPDATE werkzeug SET
|
||||
name = $2, anbieter = $3, verarbeitungsort = $4, avv_verfuegbar = $5, avv_url = $6,
|
||||
training_opt_out = $7, training_standard = $8, aufbewahrung_tage = $9,
|
||||
zertifizierungen = $10, geeignete_zwecke = $11, einschraenkungen = $12,
|
||||
letzte_pruefung = $13, quelle = $14, updated_at = now()
|
||||
zertifizierungen = $10, subprozessoren = $11, geeignete_zwecke = $12, einschraenkungen = $13,
|
||||
letzte_pruefung = $14, quelle = $15, updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING `+werkzeugColumns,
|
||||
id, in.Name, in.Anbieter, in.Verarbeitungsort, in.AVVVerfuegbar, in.AVVURL,
|
||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen,
|
||||
in.TrainingOptOut, in.TrainingStandard, in.AufbewahrungTage, in.Zertifizierungen, in.Subprozessoren,
|
||||
in.GeeigneteZwecke, in.Einschraenkungen, in.LetztePruefung, in.Quelle,
|
||||
)
|
||||
w, err := scanWerkzeug(row)
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/netcell-it/deklarix/internal/store"
|
||||
)
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
||||
func testWerkzeugInput(accountID *string, name string) store.WerkzeugInput {
|
||||
return store.WerkzeugInput{
|
||||
AccountID: accountID,
|
||||
@@ -19,8 +21,9 @@ func testWerkzeugInput(accountID *string, name string) store.WerkzeugInput {
|
||||
AVVURL: "https://beispiel.example/avv",
|
||||
TrainingOptOut: true,
|
||||
TrainingStandard: true,
|
||||
AufbewahrungTage: 30,
|
||||
AufbewahrungTage: intPtr(30),
|
||||
Zertifizierungen: []string{"ISO 27001"},
|
||||
Subprozessoren: []string{"Beispiel-Subprozessor Inc."},
|
||||
GeeigneteZwecke: []string{"Textgenerierung"},
|
||||
Einschraenkungen: nil,
|
||||
LetztePruefung: time.Now().Add(-24 * time.Hour).Truncate(time.Millisecond),
|
||||
@@ -42,6 +45,24 @@ func TestWerkzeugCRUD(t *testing.T) {
|
||||
if len(w.Zertifizierungen) != 1 || w.Zertifizierungen[0] != "ISO 27001" {
|
||||
t.Fatalf("Zertifizierungen = %v, want [ISO 27001]", w.Zertifizierungen)
|
||||
}
|
||||
if len(w.Subprozessoren) != 1 || w.Subprozessoren[0] != "Beispiel-Subprozessor Inc." {
|
||||
t.Fatalf("Subprozessoren = %v, want [Beispiel-Subprozessor Inc.]", w.Subprozessoren)
|
||||
}
|
||||
if w.AufbewahrungTage == nil || *w.AufbewahrungTage != 30 {
|
||||
t.Fatalf("AufbewahrungTage = %v, want 30", w.AufbewahrungTage)
|
||||
}
|
||||
|
||||
unbekannt, err := s.CreateWerkzeug(ctx, func() store.WerkzeugInput {
|
||||
in := testWerkzeugInput(nil, "Werkzeug ohne Aufbewahrungsangabe")
|
||||
in.AufbewahrungTage = nil
|
||||
return in
|
||||
}())
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWerkzeug (ohne Aufbewahrungsangabe): %v", err)
|
||||
}
|
||||
if unbekannt.AufbewahrungTage != nil {
|
||||
t.Fatalf("AufbewahrungTage = %v, want nil (unbekannt)", *unbekannt.AufbewahrungTage)
|
||||
}
|
||||
|
||||
got, err := s.GetWerkzeug(ctx, w.ID)
|
||||
if err != nil {
|
||||
@@ -52,12 +73,12 @@ func TestWerkzeugCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
updateInput := testWerkzeugInput(nil, "Zentrales Werkzeug (aktualisiert)")
|
||||
updateInput.AufbewahrungTage = 14
|
||||
updateInput.AufbewahrungTage = intPtr(14)
|
||||
updated, err := s.UpdateWerkzeug(ctx, w.ID, updateInput)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateWerkzeug: %v", err)
|
||||
}
|
||||
if updated.Name != "Zentrales Werkzeug (aktualisiert)" || updated.AufbewahrungTage != 14 {
|
||||
if updated.Name != "Zentrales Werkzeug (aktualisiert)" || updated.AufbewahrungTage == nil || *updated.AufbewahrungTage != 14 {
|
||||
t.Fatalf("UpdateWerkzeug = %+v, unerwartete Werte", updated)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user