diff --git a/cmd/seed-orphan-concept-drafts/main.go b/cmd/seed-orphan-concept-drafts/main.go new file mode 100644 index 0000000..bfd215b --- /dev/null +++ b/cmd/seed-orphan-concept-drafts/main.go @@ -0,0 +1,342 @@ +// Command seed-orphan-concept-drafts stages draft sequencing_rules for +// deadline_concepts that have rule_count=0 ("orphans"). It calls the +// same services.RuleEditorService.Create that POST +// /admin/api/procedural-events runs internally, so the audit trigger +// + INSTEAD-OF view trigger fan-out into procedural_events + +// sequencing_rules + legal_sources fire identically. No HTTP/auth +// shell, no direct SQL writes by this command. +// +// All rules are created with lifecycle_state='draft' (forced by the +// service). The admin still reviews + publishes via +// /admin/procedural-events. +// +// t-paliad-320: editorial backlog from t-paliad-193, four remaining +// orphan concepts: counterclaim-for-revocation, versaeumnisurteil- +// einspruch, schriftsatznachreichung, weiterbehandlung. The +// weiterbehandlung concept gets two drafts (EPC Art. 121 + R. 135 +// versus DPatG § 123a) since the two regimes have different durations +// and jurisdictions. +// +// Usage: +// +// DATABASE_URL=postgres://… go run ./cmd/seed-orphan-concept-drafts \ +// [-dry-run] [-reason "free-text audit reason"] +// +// Idempotency: the command refuses to insert if any rule for a given +// (concept, proceeding_type, rule_code) already exists. Safe to re-run +// after a partial failure. +package main + +import ( + "context" + "database/sql" + "errors" + "flag" + "fmt" + "log" + "os" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + + "mgit.msbls.de/m/paliad/internal/services" +) + +// draftSpec captures one CreateRuleInput plus the metadata the command +// needs to resolve concept_id + proceeding_type_id from human-readable +// slugs/codes. ProceedingCode == "" means event-rooted +// (proceeding_type_id = NULL), used for cross-cutting rules whose +// jurisdiction has no matching proceeding_type yet. +type draftSpec struct { + Label string // human label for log output + ConceptSlug string + ProceedingCode string // "" → NULL proceeding_type_id (event-rooted) + SubmissionCode string + Name string + NameEN string + EventKind string + PrimaryParty string // "" → omit (NULL) + DurationValue int + DurationUnit string + Timing string + Priority string + IsCourtSet bool + RuleCode string + LegalSource string + DeadlineNotes string + DeadlineNotesEn string +} + +func drafts() []draftSpec { + return []draftSpec{ + // ─── 1. counterclaim-for-revocation (UPC R.25.1 ∧ R.23) ─────── + { + Label: "counterclaim-for-revocation → upc.ccr.cfi", + ConceptSlug: "counterclaim-for-revocation", + ProceedingCode: "upc.ccr.cfi", + SubmissionCode: "upc.ccr.cfi.lodge", + Name: "Widerklage auf Nichtigkeit (CCR)", + NameEN: "Counterclaim for Revocation (CCR)", + EventKind: "filing", + PrimaryParty: "defendant", + DurationValue: 3, + DurationUnit: "months", + Timing: "after", + Priority: "mandatory", + IsCourtSet: false, + RuleCode: "RoP.025", + LegalSource: "UPC.RoP.25.1", + DeadlineNotes: "Die Widerklage auf Nichtigkeit (Counterclaim for Revocation, CCR) ist gemeinsam mit der Klageerwiderung (Statement of Defence) einzureichen — d. h. innerhalb von 3 Monaten ab Zustellung der Klageschrift " + + "(R. 23 i. V. m. R. 25.1 RoP). Inhaltliche Anforderungen folgen R. 25-30 RoP (insbes. R. 25.1(a)-(c) zu Antrag, Tatsachen und Beweismitteln; R. 27 zu Verfahren nach Einreichung; R. 30 zu einem Antrag auf Änderung des Patents).", + DeadlineNotesEn: "The Counterclaim for Revocation (CCR) must be lodged together with the Statement of Defence — i.e. within 3 months of service of the Statement of Claim " + + "(Rule 23 in conjunction with Rule 25.1 RoP). Substantive requirements follow Rules 25-30 RoP (in particular R. 25.1(a)-(c) on the application, facts and evidence; R. 27 on post-filing procedure; R. 30 on any application to amend the patent).", + }, + + // ─── 2. versaeumnisurteil-einspruch (ZPO § 339) ─────────────── + { + Label: "versaeumnisurteil-einspruch → de.inf.lg", + ConceptSlug: "versaeumnisurteil-einspruch", + ProceedingCode: "de.inf.lg", + SubmissionCode: "de.inf.lg.einspruch_vu", + Name: "Einspruch gegen Versäumnisurteil", + NameEN: "Objection to default judgment", + EventKind: "filing", + PrimaryParty: "defendant", + DurationValue: 2, + DurationUnit: "weeks", + Timing: "after", + Priority: "mandatory", + IsCourtSet: false, + RuleCode: "§ 339 ZPO", + LegalSource: "DE.ZPO.339.1", + DeadlineNotes: "Notfrist von 2 Wochen ab Zustellung des Versäumnisurteils (§ 339(1) ZPO). " + + "Bei Auslandszustellung oder öffentlicher Bekanntmachung bestimmt das Gericht die Einspruchsfrist gesondert im Versäumnisurteil oder durch nachträglichen Beschluss (§ 339(2) ZPO) — in diesem Fall die gerichtlich festgesetzte Frist mit „Datum setzen“ überschreiben. " + + "Form: schriftlich oder zu Protokoll der Geschäftsstelle (§ 340(1) ZPO); die Einspruchsbegründung kann bis zum Verhandlungstermin nachgereicht werden (§ 340(3) ZPO).", + DeadlineNotesEn: "Statutory two-week emergency period (Notfrist) from service of the default judgment (§ 339(1) ZPO). " + + "If service is abroad or by public notice, the court sets the objection period separately in the default judgment or by a subsequent order (§ 339(2) ZPO) — in that case override with the court-set date. " + + "Form: in writing or before the registry clerk (§ 340(1) ZPO); substantive grounds may be filed up to the oral hearing (§ 340(3) ZPO).", + }, + + // ─── 3. schriftsatznachreichung (ZPO § 283) ─────────────────── + { + Label: "schriftsatznachreichung → de.inf.lg", + ConceptSlug: "schriftsatznachreichung", + ProceedingCode: "de.inf.lg", + SubmissionCode: "de.inf.lg.nachreichung", + Name: "Schriftsatznachreichung", + NameEN: "Subsequent written submission", + EventKind: "filing", + PrimaryParty: "", // concept.party = "both" → no default + DurationValue: 3, + DurationUnit: "weeks", + Timing: "after", + Priority: "optional", + IsCourtSet: true, + RuleCode: "§ 283 ZPO", + LegalSource: "DE.ZPO.283", + DeadlineNotes: "Vom Gericht in der mündlichen Verhandlung gesetzte Schriftsatzfrist gem. § 283 ZPO. " + + "Sie wird nur auf Antrag einer Partei bestimmt, die sich auf neues Vorbringen des Gegners nicht erklären konnte. " + + "Die konkrete Frist (in der Praxis 2-3 Wochen) und der nachfolgende Verkündungstermin werden im Sitzungsprotokoll bzw. in der prozessleitenden Verfügung festgelegt — Default-Frist hier 3 Wochen, mit „Datum setzen“ überschreiben, sobald die Verfügung vorliegt. " + + "Nach Fristablauf darf das Gericht keine weiteren Erklärungen mehr berücksichtigen (§ 283 S. 2, § 296a ZPO).", + DeadlineNotesEn: "Court-set written-submission period under § 283 ZPO, granted on a party's application when it could not respond at the oral hearing to the opponent's new submissions. " + + "The actual period (in practice 2-3 weeks) and the announcement date are set in the hearing record / case-management order — default 3 weeks here, override via „set date“ once the order is on the file. " + + "After expiry, the court will disregard further submissions (§ 283 sent. 2, § 296a ZPO).", + }, + + // ─── 4. weiterbehandlung — EPC variant (Art. 121 + R. 135) ──── + { + Label: "weiterbehandlung (EPC) → epa.grant.exa", + ConceptSlug: "weiterbehandlung", + ProceedingCode: "epa.grant.exa", + SubmissionCode: "epa.grant.exa.weiterbeh", + Name: "Antrag auf Weiterbehandlung", + NameEN: "Request for further processing", + EventKind: "filing", + PrimaryParty: "claimant", + DurationValue: 2, + DurationUnit: "months", + Timing: "after", + Priority: "mandatory", + IsCourtSet: false, + RuleCode: "Art. 121 EPÜ", + LegalSource: "EU.EPC-R.135.1", + DeadlineNotes: "Antrag auf Weiterbehandlung gem. Art. 121 EPÜ i. V. m. R. 135(1) EPÜ — 2 Monate ab Zustellung der Mitteilung über die Fristversäumung bzw. den eingetretenen Rechtsverlust. " + + "Der Antrag wird durch Zahlung der vorgeschriebenen Weiterbehandlungsgebühr gestellt; die versäumte Handlung muss innerhalb derselben 2-Monats-Frist nachgeholt werden (R. 135(1) EPÜ). " + + "Die Frist ist nicht verlängerbar. Ausgeschlossen sind insbesondere die Frist für die Weiterbehandlung selbst sowie die in R. 135(2) EPÜ ausdrücklich aufgeführten Fristen (u. a. die Beschwerdefrist nach Art. 108 EPÜ, die Prioritätsfrist nach Art. 87 EPÜ und die Frist zur Wiedereinsetzung).", + DeadlineNotesEn: "Request for further processing under Article 121 EPC in conjunction with Rule 135(1) EPC — two months from notification of the communication concerning the missed time limit or the loss of rights. " + + "The request is made by payment of the further-processing fee; the omitted act must be completed within the same two-month period (Rule 135(1) EPC). " + + "The period is non-extendable. Excluded: the further-processing period itself and the periods listed in Rule 135(2) EPC (notably the appeal period under Art. 108 EPC, the priority period under Art. 87 EPC, and the re-establishment period).", + }, + + // ─── 5. weiterbehandlung — DPatG § 123a variant ─────────────── + // No `dpma.grant.*` proceeding_type exists yet, so this rule is + // event-rooted (proceeding_type_id NULL) — same pattern as 78 + // other cross-cutting rules. Editorial follow-up: create a + // `dpma.grant.dpma` proceeding_type and reassign. + { + Label: "weiterbehandlung (DPatG § 123a) → event-rooted (NULL proceeding_type)", + ConceptSlug: "weiterbehandlung", + ProceedingCode: "", // event-rooted + SubmissionCode: "dpma.grant.weiterbeh", + Name: "Antrag auf Weiterbehandlung (DPMA)", + NameEN: "Request for further processing (DPMA, § 123a PatG)", + EventKind: "filing", + PrimaryParty: "claimant", + DurationValue: 1, + DurationUnit: "months", + Timing: "after", + Priority: "mandatory", + IsCourtSet: false, + RuleCode: "§ 123a PatG", + LegalSource: "DE.PatG.123a.1", + DeadlineNotes: "Antrag auf Weiterbehandlung einer DPMA-Patentanmeldung gem. § 123a PatG — 1 Monat ab Zustellung der Mitteilung über die Rechtsfolge der Fristversäumung. " + + "Innerhalb dieser Frist müssen (i) der Antrag schriftlich gestellt, (ii) die versäumte Handlung nachgeholt und (iii) die Weiterbehandlungsgebühr nach Patentkostengesetz (PatKostG) gezahlt werden. " + + "§ 123a PatG erfasst ausschließlich Anmeldungsfristen, deren Versäumung kraft Gesetzes die Zurückweisung der Anmeldung zur Folge hat. Für sonstige Fristversäumnisse kommt nur die Wiedereinsetzung nach § 123 PatG in Betracht (1 Monat ab Wegfall des Hindernisses, max. 1 Jahr ab Fristablauf). " + + "HINWEIS — Taxonomie: bisher kein dpma.grant.* proceeding_type vorhanden; Regel daher event-rooted (proceeding_type_id NULL). Editorial follow-up: dpma.grant.dpma proceeding_type anlegen und diese Regel umhängen.", + DeadlineNotesEn: "Request for further processing of a DPMA patent application under § 123a PatG — 1 month from notification of the consequence of the missed deadline. " + + "Within this period the applicant must (i) file the written request, (ii) complete the omitted act, and (iii) pay the further-processing fee under the German Patent Costs Act (PatKostG). " + + "§ 123a PatG covers only application-stage deadlines whose statutory consequence is rejection. For other missed deadlines, re-establishment under § 123 PatG is the only route (1 month from removal of the obstacle, max 1 year from the missed deadline). " + + "TAXONOMY NOTE: no dpma.grant.* proceeding_type exists yet; this rule is event-rooted (proceeding_type_id NULL). Editorial follow-up: create a dpma.grant.dpma proceeding_type and reassign this rule.", + }, + } +} + +func main() { + dryRun := flag.Bool("dry-run", false, "log the planned drafts but do not write") + reason := flag.String("reason", "t-paliad-320: editorial seed of orphan deadline-concept rules (researcher darwin + lex)", "audit reason recorded with each Create()") + flag.Parse() + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL not set — export the paliad postgres URL before running") + } + + ctx := context.Background() + conn, err := sqlx.Connect("postgres", dbURL) + if err != nil { + log.Fatalf("connect db: %v", err) + } + defer conn.Close() + + rules := services.NewDeadlineRuleService(conn) + editor := services.NewRuleEditorService(conn, rules) + + conceptIDs := map[string]uuid.UUID{} + proceedingIDs := map[string]int{} + specs := drafts() + + for _, s := range specs { + if _, ok := conceptIDs[s.ConceptSlug]; ok { + continue + } + var id uuid.UUID + if err := conn.GetContext(ctx, &id, + `SELECT id FROM paliad.deadline_concepts WHERE slug = $1`, s.ConceptSlug); err != nil { + log.Fatalf("lookup concept %q: %v", s.ConceptSlug, err) + } + conceptIDs[s.ConceptSlug] = id + } + for _, s := range specs { + if s.ProceedingCode == "" { + continue + } + if _, ok := proceedingIDs[s.ProceedingCode]; ok { + continue + } + var id int + if err := conn.GetContext(ctx, &id, + `SELECT id FROM paliad.proceeding_types WHERE code = $1`, s.ProceedingCode); err != nil { + log.Fatalf("lookup proceeding_type %q: %v", s.ProceedingCode, err) + } + proceedingIDs[s.ProceedingCode] = id + } + + fmt.Printf("Seeding %d drafts (dry-run=%v)\n", len(specs), *dryRun) + + for i, s := range specs { + conceptID := conceptIDs[s.ConceptSlug] + var procID *int + if s.ProceedingCode != "" { + p := proceedingIDs[s.ProceedingCode] + procID = &p + } + + // Idempotency: refuse if a rule with the same (concept, proceeding, + // rule_code) already exists in any lifecycle state. + if existing, err := findExisting(ctx, conn, conceptID, procID, s.RuleCode); err != nil { + log.Fatalf("[%d] idempotency check failed for %s: %v", i+1, s.Label, err) + } else if existing != uuid.Nil { + fmt.Printf(" [%d] SKIP %s — already exists as %s\n", i+1, s.Label, existing) + continue + } + + input := services.CreateRuleInput{ + Name: s.Name, + NameEN: s.NameEN, + ProceedingTypeID: procID, + DurationValue: s.DurationValue, + DurationUnit: s.DurationUnit, + Priority: s.Priority, + IsCourtSet: s.IsCourtSet, + } + input.ConceptID = &conceptID + code := s.SubmissionCode + input.SubmissionCode = &code + ek := s.EventKind + input.EventType = &ek + t := s.Timing + input.Timing = &t + rc := s.RuleCode + input.RuleCode = &rc + ls := s.LegalSource + input.LegalSource = &ls + dn := s.DeadlineNotes + input.DeadlineNotes = &dn + dne := s.DeadlineNotesEn + input.DeadlineNotesEn = &dne + if s.PrimaryParty != "" { + pp := s.PrimaryParty + input.PrimaryParty = &pp + } + + if *dryRun { + fmt.Printf(" [%d] DRY %s (concept=%s, proc=%s, code=%s, %d %s, %s)\n", + i+1, s.Label, conceptID, codeOrNil(procID), code, s.DurationValue, s.DurationUnit, s.RuleCode) + continue + } + + row, err := editor.Create(ctx, input, *reason) + if err != nil { + log.Fatalf(" [%d] CREATE failed for %s: %v", i+1, s.Label, err) + } + fmt.Printf(" [%d] OK %s → id=%s lifecycle=%s\n", + i+1, s.Label, row.ID, row.LifecycleState) + } + + fmt.Println("Done.") +} + +func findExisting(ctx context.Context, conn *sqlx.DB, conceptID uuid.UUID, procID *int, ruleCode string) (uuid.UUID, error) { + var id uuid.UUID + q := ` + SELECT sr.id + FROM paliad.sequencing_rules sr + JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id + WHERE pe.concept_id = $1 + AND sr.rule_code IS NOT DISTINCT FROM $2 + AND sr.proceeding_type_id IS NOT DISTINCT FROM $3 + LIMIT 1` + err := conn.GetContext(ctx, &id, q, conceptID, ruleCode, procID) + if errors.Is(err, sql.ErrNoRows) { + return uuid.Nil, nil + } + return id, err +} + +func codeOrNil(p *int) string { + if p == nil { + return "" + } + return fmt.Sprintf("%d", *p) +}