// Seed Example Projects (t-paliad-256 / m/paliad#87). // // Re-runnable test-data reset: // // 1. Wipes every row in paliad.projects (FK CASCADE handles the // dependent rows: deadlines, appointments, parties, notes, // project_events, project_teams, submission_drafts, approval_*, // project_partner_units, user_pinned_projects, documents, // user_calendar_bindings). // // 2. Inserts a small but realistic example tree (3 clients, 4 // litigations, 4 patents, 8 cases — 19 projects total) that // exercises the auto-derived chain code: Client.Litigation.Patent.Case // → e.g. SIEMENS.HUAW.789.INF.CFI. // // 3. Re-reads the projects and prints each row's chain code so the // operator can eyeball the result without bouncing to SQL. // // Reference tables (proceeding_types, deadline_rules, event_types, // gerichte, checklists templates, firms, profiles) are untouched. // // Run: // // DATABASE_URL='postgres://...' go run ./scripts/seed-example-projects // // One transaction wraps both wipe and seed so the DB is never in a // half-wiped state. Re-running drops the previous example tree and // reseeds fresh UUIDs — handy when project-code semantics change. // // Owner: m (matthias.siebels@hoganlovells.com). The script looks the // auth user up by email so it works on any environment where that // account exists; on a brand-new DB it falls back to NULL created_by. package main import ( "context" "database/sql" "errors" "flag" "fmt" "os" "strings" "text/tabwriter" "time" "github.com/google/uuid" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" "mgit.msbls.de/m/paliad/internal/services" ) // ownerEmail is the auth.users email the seed assigns as created_by. // Living in code (not a flag) because the example tree is m-owned by // convention; flip if the example data ever needs a service-account // owner. const ownerEmail = "matthias.siebels@hoganlovells.com" // Proceeding-type IDs used by the seed. Resolved by code (not pinned // to integer IDs in source) to survive DB renumbering. Loaded once at // startup; missing codes fail fast with a clear message. var proceedingCodes = []string{ "upc.inf.cfi", "upc.ccr.cfi", "upc.apl.merits", "de.inf.lg", "epa.opp.opd", "de.null.bpatg", "dpma.opp.dpma", } func main() { dsn := flag.String("dsn", os.Getenv("DATABASE_URL"), "Postgres DSN (defaults to $DATABASE_URL)") dryRun := flag.Bool("dry-run", false, "print intended actions, roll back transaction") flag.Parse() if *dsn == "" { fmt.Fprintln(os.Stderr, "seed-example-projects: DATABASE_URL not set and -dsn empty") os.Exit(1) } db, err := sqlx.Connect("postgres", *dsn) if err != nil { fmt.Fprintln(os.Stderr, "connect:", err) os.Exit(1) } defer db.Close() ctx := context.Background() if err := run(ctx, db, *dryRun); err != nil { fmt.Fprintln(os.Stderr, "seed-example-projects:", err) os.Exit(1) } } func run(ctx context.Context, db *sqlx.DB, dryRun bool) error { ownerID, err := lookupOwner(ctx, db, ownerEmail) if err != nil { return fmt.Errorf("lookup owner: %w", err) } if ownerID == uuid.Nil { fmt.Printf("note: %s not found in auth.users — created_by will be NULL\n", ownerEmail) } else { fmt.Printf("owner resolved: %s = %s\n", ownerEmail, ownerID) } procIDs, err := lookupProceedingTypes(ctx, db, proceedingCodes) if err != nil { return fmt.Errorf("lookup proceeding_types: %w", err) } tx, err := db.BeginTxx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer func() { _ = tx.Rollback() }() // no-op if Commit ran first if err := wipe(ctx, tx); err != nil { return fmt.Errorf("wipe: %w", err) } tree, err := seed(ctx, tx, ownerID, procIDs) if err != nil { return fmt.Errorf("seed: %w", err) } if dryRun { fmt.Println("\n--- DRY RUN — rolling back ---") return nil } if err := tx.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } fmt.Println("seed committed.") if err := report(ctx, db, tree); err != nil { return fmt.Errorf("report: %w", err) } return nil } func lookupOwner(ctx context.Context, db *sqlx.DB, email string) (uuid.UUID, error) { var id uuid.UUID err := db.GetContext(ctx, &id, `SELECT id FROM auth.users WHERE email = $1`, email) if errors.Is(err, sql.ErrNoRows) { return uuid.Nil, nil } if err != nil { return uuid.Nil, err } return id, nil } func lookupProceedingTypes(ctx context.Context, db *sqlx.DB, codes []string) (map[string]int, error) { rows, err := db.QueryxContext(ctx, `SELECT id, code FROM paliad.proceeding_types WHERE code = ANY($1)`, pgTextArray(codes)) if err != nil { return nil, err } defer rows.Close() out := make(map[string]int, len(codes)) for rows.Next() { var id int var code string if err := rows.Scan(&id, &code); err != nil { return nil, err } out[code] = id } for _, c := range codes { if _, ok := out[c]; !ok { return nil, fmt.Errorf("proceeding_types row missing for code=%q", c) } } return out, nil } // pgTextArray is the lib/pq array adapter, repackaged inline so the // script doesn't need a separate util import. func pgTextArray(xs []string) any { type arr = []string return arr(xs) } // wipe deletes every paliad.projects row. FK CASCADE handles the // dependent tables (verified live 2026-05-25 against information_schema: // appointments, approval_requests, approval_policies, deadlines, // documents, notes, parties, project_events, project_partner_units, // project_teams, submission_drafts, user_pinned_projects, // user_calendar_bindings, checklist_shares all cascade; projects. // counterclaim_of and checklist_instances SET NULL; policy_audit_log // SET NULL). // // Reference tables (proceeding_types, deadline_rules, event_types, // gerichte, checklists, firms, partner_units, profiles) are not // referenced from this delete. func wipe(ctx context.Context, tx *sqlx.Tx) error { res, err := tx.ExecContext(ctx, `DELETE FROM paliad.projects`) if err != nil { return err } n, _ := res.RowsAffected() fmt.Printf("wiped: %d project rows (FK CASCADE handled dependents)\n", n) return nil } // seededNode is one row of the seed result, kept so we can print the // chain code after commit without re-querying for IDs. type seededNode struct { id uuid.UUID title string } // seed inserts the example tree. Order matters because parent_id FKs // must already exist — clients first, then litigations under them, then // patents, then cases (with the CCR case referencing its sibling // Klage case via counterclaim_of). func seed(ctx context.Context, tx *sqlx.Tx, ownerID uuid.UUID, procIDs map[string]int) ([]seededNode, error) { var nodes []seededNode insertProject := func(p projectInsert) (uuid.UUID, error) { id := uuid.New() var createdBy any if ownerID != uuid.Nil { createdBy = ownerID } _, err := tx.ExecContext(ctx, ` INSERT INTO paliad.projects ( id, type, parent_id, title, reference, description, status, created_by, industry, country, client_number, matter_number, patent_number, filing_date, grant_date, court, case_number, proceeding_type_id, our_side, opponent_code, instance_level, counterclaim_of ) VALUES ( $1, $2, $3, $4, $5, $6, 'active', $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21 )`, id, p.Type, nullUUID(p.ParentID), p.Title, nullStr(p.Reference), nullStr(p.Description), createdBy, nullStr(p.Industry), nullStr(p.Country), nullStr(p.ClientNumber), nullStr(p.MatterNumber), nullStr(p.PatentNumber), nullDate(p.FilingDate), nullDate(p.GrantDate), nullStr(p.Court), nullStr(p.CaseNumber), nullInt(p.ProceedingTypeID), nullStr(p.OurSide), nullStr(p.OpponentCode), nullStr(p.InstanceLevel), nullUUID(p.CounterclaimOf), ) if err != nil { return uuid.Nil, fmt.Errorf("insert %s %q: %w", p.Type, p.Title, err) } nodes = append(nodes, seededNode{id: id, title: p.Title}) return id, nil } // --- Client 1: Siemens AG ---------------------------------------- siemens, err := insertProject(projectInsert{ Type: "client", Title: "Siemens AG", Reference: "SIEMENS", Industry: "Telekommunikation / Industrieelektronik", Country: "DE", Description: "Beispiel-Mandant — Telekommunikation & Halbleiter.", }) if err != nil { return nil, err } siemensHuawei, err := insertProject(projectInsert{ Type: "litigation", ParentID: siemens, Title: "Siemens ./. Huawei Technologies", OpponentCode: "HUAW", Description: "Patentstreit Mobilfunk-Standardpatent.", OurSide: "claimant", }) if err != nil { return nil, err } siemensHuaweiPatent, err := insertProject(projectInsert{ Type: "patent", ParentID: siemensHuawei, Title: "EP3456789 — Funkkommunikationssystem mit Mehrfachantenne", PatentNumber: "EP3456789", FilingDate: "2018-03-12", GrantDate: "2022-11-09", }) if err != nil { return nil, err } upcInfCFI, err := insertProject(projectInsert{ Type: "case", ParentID: siemensHuaweiPatent, Title: "UPC CFI München — Klage Siemens ./. Huawei (EP3456789)", Court: "UPC Lokalkammer München", CaseNumber: "UPC_CFI_123/2026", ProceedingTypeID: procIDs["upc.inf.cfi"], OurSide: "claimant", InstanceLevel: "first", }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: siemensHuaweiPatent, Title: "UPC CFI München — Widerklage Huawei ./. Siemens (EP3456789)", Court: "UPC Lokalkammer München", CaseNumber: "UPC_CFI_123/2026 (CCR)", ProceedingTypeID: procIDs["upc.ccr.cfi"], OurSide: "defendant", // we're respondent on the CCR InstanceLevel: "first", CounterclaimOf: upcInfCFI, }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: siemensHuaweiPatent, Title: "UPC Berufungsgericht — Berufung Huawei (EP3456789)", Court: "UPC Court of Appeal", CaseNumber: "UPC_CoA_45/2027", ProceedingTypeID: procIDs["upc.apl.merits"], OurSide: "respondent", InstanceLevel: "appeal", }) if err != nil { return nil, err } siemensBosch, err := insertProject(projectInsert{ Type: "litigation", ParentID: siemens, Title: "Siemens ./. Robert Bosch GmbH", OpponentCode: "BOSCH", Description: "Sensorik / autonomes Fahren.", OurSide: "claimant", }) if err != nil { return nil, err } siemensBoschPatent, err := insertProject(projectInsert{ Type: "patent", ParentID: siemensBosch, Title: "EP1111222 — Sensoreinrichtung für autonomes Fahren", PatentNumber: "EP1111222", FilingDate: "2017-06-21", GrantDate: "2021-08-04", }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: siemensBoschPatent, Title: "LG München I — Klage Siemens ./. Bosch (EP1111222)", Court: "Landgericht München I", CaseNumber: "7 O 12345/26", ProceedingTypeID: procIDs["de.inf.lg"], OurSide: "claimant", InstanceLevel: "first", }) if err != nil { return nil, err } // --- Client 2: Bayer AG ------------------------------------------ bayer, err := insertProject(projectInsert{ Type: "client", Title: "Bayer AG", Reference: "BAYER", Industry: "Pharma / Life Sciences", Country: "DE", Description: "Beispiel-Mandant — pharmazeutische Wirkstoffe.", }) if err != nil { return nil, err } bayerNova, err := insertProject(projectInsert{ Type: "litigation", ParentID: bayer, Title: "Bayer ./. Novartis Pharma", OpponentCode: "NOVA", Description: "Wirkstoffverbindung X — Einspruch + Nichtigkeit.", OurSide: "claimant", }) if err != nil { return nil, err } bayerNovaPatent, err := insertProject(projectInsert{ Type: "patent", ParentID: bayerNova, Title: "EP2222333 — Wirkstoffverbindung X", PatentNumber: "EP2222333", FilingDate: "2015-09-30", GrantDate: "2020-04-22", }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: bayerNovaPatent, Title: "EPA Einspruch — Novartis ./. EP2222333", Court: "Europäisches Patentamt — Einspruchsabteilung", CaseNumber: "OPP-2026-0042", ProceedingTypeID: procIDs["epa.opp.opd"], OurSide: "respondent", // Bayer is patent owner defending the patent InstanceLevel: "first", }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: bayerNovaPatent, Title: "BPatG — Nichtigkeitsklage Novartis ./. EP2222333", Court: "Bundespatentgericht", CaseNumber: "5 Ni 12/26", ProceedingTypeID: procIDs["de.null.bpatg"], OurSide: "respondent", InstanceLevel: "first", }) if err != nil { return nil, err } // --- Client 3: Beispiel AG (intentionally sparse) ---------------- // Demonstrates the empty-segment skip in BuildProjectCode — the // case row has a proceeding_type set so the tail is present, but // no instance_level / our_side, and the patent's number is national // (DE) so the last-3-digits segment shows DE-style behaviour. beispiel, err := insertProject(projectInsert{ Type: "client", Title: "Beispiel AG", Reference: "BEISPL", Industry: "Unspezifiziert", Country: "DE", Description: "Sparse-Beispiel — zeigt, wie fehlende Segmente übersprungen werden.", }) if err != nil { return nil, err } beispielWtb, err := insertProject(projectInsert{ Type: "litigation", ParentID: beispiel, Title: "Beispiel ./. Wettbewerber GmbH", OpponentCode: "WTB", Description: "Demo-Litigation ohne große Detailtiefe.", }) if err != nil { return nil, err } beispielWtbPatent, err := insertProject(projectInsert{ Type: "patent", ParentID: beispielWtb, Title: "DE10987654 — Demo-Erfindung", PatentNumber: "DE10987654", }) if err != nil { return nil, err } _, err = insertProject(projectInsert{ Type: "case", ParentID: beispielWtbPatent, Title: "DPMA Einspruch — Wettbewerber ./. DE10987654", Court: "Deutsches Patent- und Markenamt", CaseNumber: "DPMA-EIN-987/26", ProceedingTypeID: procIDs["dpma.opp.dpma"], OurSide: "respondent", InstanceLevel: "first", }) if err != nil { return nil, err } fmt.Printf("seeded: %d projects\n", len(nodes)) return nodes, nil } // projectInsert is the typed input for one insertProject call. Pointer // fields are kept as plain strings here and converted via nullStr at // bind time; keeps the call sites readable. type projectInsert struct { Type string ParentID uuid.UUID Title string Reference string Description string Industry string Country string ClientNumber string MatterNumber string PatentNumber string FilingDate string // YYYY-MM-DD GrantDate string Court string CaseNumber string ProceedingTypeID int OurSide string OpponentCode string InstanceLevel string CounterclaimOf uuid.UUID } func nullStr(s string) any { if s == "" { return nil } return s } func nullInt(i int) any { if i == 0 { return nil } return i } func nullUUID(u uuid.UUID) any { if u == uuid.Nil { return nil } return u } func nullDate(s string) any { if s == "" { return nil } t, err := time.Parse("2006-01-02", s) if err != nil { return nil } return t } // reportRow is one row of the post-seed report — only the fields the // printout needs. type reportRow struct { ID uuid.UUID `db:"id"` Type string `db:"type"` Title string `db:"title"` Path string `db:"path"` } // report prints the seeded tree with the auto-derived chain code for // each row. Uses services.BuildProjectCode so the script verifies the // same helper the live app uses (catches drift if the algorithm // changes). func report(ctx context.Context, db *sqlx.DB, _ []seededNode) error { var rows []reportRow err := db.SelectContext(ctx, &rows, ` SELECT id, type, title, path FROM paliad.projects ORDER BY path `) if err != nil { return err } fmt.Println("\nresulting chain codes:") tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "TYPE\tTITLE\tCODE") for _, r := range rows { code, err := services.BuildProjectCode(ctx, db, r.ID) if err != nil { return fmt.Errorf("build code for %s: %w", r.ID, err) } indent := strings.Repeat(" ", pathDepth(r.Path)-1) fmt.Fprintf(tw, "%s\t%s%s\t%s\n", r.Type, indent, r.Title, code) } return tw.Flush() } func pathDepth(p string) int { if p == "" { return 1 } d := 1 for _, c := range p { if c == '.' { d++ } } return d }