feat(procedural-events): t-paliad-262 Slice A — prose-only rename (m/paliad#93)

Renames the procedural-event surface of paliad.deadline_rules from
"rule" wording to "procedural event" / "Verfahrensschritt" wording.
No DB change, no API change, no Go-type rename. Fully reversible.

m's locks via head (2026-05-25):
- Q1=C: cosmetic now, structural rework (Slice B) as planned t-paliad-273.
- Q2: umbrella term = procedural event / Verfahrensschritt.
- Q7: legacy {{rule.X}} placeholder aliases kept forever (@deprecated).
- Q9: Slice B filed as on-hold task immediately.

Changes:
- internal/services/submission_vars.go: emit procedural_event.* keys
  alongside legacy rule.* keys with identical values. Package + function
  comments updated. Function name kept (addRuleVars) to avoid coupling
  Slice A to the Go-type rename which is Slice B (B.5).
- internal/services/submission_vars_aliases_test.go (new): regression
  test asserts (a) every (canonical, legacy) key pair resolves to the
  same string for both DE and EN; (b) NULL source columns still emit
  both keys with "". Removing either guard surfaces here.
- frontend/src/client/submission-draft.ts: placeholder catalog now
  shows canonical procedural_event.* labels first; legacy rule.*
  entries kept as "(legacy)"-marked aliases.
- frontend/src/client/i18n.ts: admin labels updated in place
  ("Regeln verwalten" → "Verfahrensschritte verwalten", etc.) under
  existing admin.rules.* keys; canonical admin.procedural_events.*
  keys added with identical values so .tsx files can rebind in Slice B.
- frontend/src/i18n-keys.ts: auto-regenerated by build pipeline.

Design doc: docs/design-procedural-events-model-2026-05-25.md (shipped
on the inventor branch mai/cronus/inventor-procedural).

Slice B (planned, on-hold): t-paliad-273.
This commit is contained in:
mAi
2026-05-25 16:03:03 +02:00
parent 247e9005db
commit d52995a4d6
5 changed files with 338 additions and 79 deletions

View File

@@ -2841,21 +2841,26 @@ const translations: Record<Lang, Record<string, string>> = {
"views.bar.save.error.network": "Netzwerkfehler — bitte erneut versuchen.",
// t-paliad-192 Slice 11b — Admin rule-editor UI.
"nav.admin.rules": "Regeln verwalten",
"nav.admin.rules_export": "Regel-Migrations",
"admin.card.rules.title": "Regeln verwalten",
"admin.card.rules.desc": "Fristen-Regeln anlegen, bearbeiten, publishen. Audit-Log, Preview, Migration-Export.",
// t-paliad-262 Slice A — "Regel" relabelled as "Verfahrensschritt".
// The admin URL `/admin/rules` and i18n key prefix `admin.rules.*` stay
// (URL change is Slice B.6); the visible labels rename. Canonical
// `admin.procedural_events.*` aliases live after the EN block — they
// pin the contract for when .tsx files rebind in Slice B (B.5).
"nav.admin.rules": "Verfahrensschritte verwalten",
"nav.admin.rules_export": "Verfahrensschritt-Migrations",
"admin.card.rules.title": "Verfahrensschritte verwalten",
"admin.card.rules.desc": "Verfahrensschritte anlegen, bearbeiten, publishen. Audit-Log, Preview, Migration-Export.",
"admin.rules.list.title": "Regeln verwalten — Paliad",
"admin.rules.list.heading": "Regeln verwalten",
"admin.rules.list.subtitle": "Fristen-Regeln anlegen, bearbeiten und freigeben. Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ Neue Regel",
"admin.rules.list.title": "Verfahrensschritte verwalten — Paliad",
"admin.rules.list.heading": "Verfahrensschritte verwalten",
"admin.rules.list.subtitle": "Verfahrensschritte (Schriftsätze, Anhörungen, Entscheidungen, …) anlegen, bearbeiten und freigeben. Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ Neuer Verfahrensschritt",
"admin.rules.list.export": "Migrations exportieren",
"admin.rules.tab.rules": "Regeln",
"admin.rules.tab.orphans": "Orphans",
"admin.rules.loading": "Lade…",
"admin.rules.empty": "Keine Regeln für die gewählten Filter.",
"admin.rules.error.load": "Konnte Regeln nicht laden.",
"admin.rules.empty": "Keine Verfahrensschritte für die gewählten Filter.",
"admin.rules.error.load": "Konnte Verfahrensschritte nicht laden.",
"admin.rules.filter.proceeding": "Verfahrenstyp",
"admin.rules.filter.proceeding.any": "Alle",
@@ -2866,7 +2871,7 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.filter.search": "Suche",
"admin.rules.filter.search.placeholder": "Name, Submission Code, Rechtsgrundlage…",
"admin.rules.col.submission_code": "Submission Code / Einreichung-Kennung",
"admin.rules.col.submission_code": "Code (Verfahrensschritt)",
"admin.rules.col.legal_citation": "Rechtsgrundlage",
"admin.rules.col.name": "Name",
"admin.rules.col.proceeding": "Verfahrenstyp",
@@ -2896,8 +2901,8 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.orphans.reason.manual_unbound": "Manuell entkoppelt",
"admin.rules.orphans.resolved": "Orphan zugeordnet.",
"admin.rules.modal.new.title": "Neue Regel anlegen",
"admin.rules.modal.new.body": "Eine neue Regel wird als Draft angelegt. Bitte einen Grund (mind. 10 Zeichen) angeben — dieser wandert ins Audit-Log und beim Export in die Migration.",
"admin.rules.modal.new.title": "Neuen Verfahrensschritt anlegen",
"admin.rules.modal.new.body": "Ein neuer Verfahrensschritt wird als Draft angelegt. Bitte einen Grund (mind. 10 Zeichen) angeben — dieser wandert ins Audit-Log und beim Export in die Migration.",
"admin.rules.modal.resolve.title": "Orphan zuordnen",
"admin.rules.modal.resolve.body": "Bitte einen Grund (mind. 10 Zeichen) angeben. Die Regel-Verknüpfung wird sofort auf der Deadline gespeichert.",
"admin.rules.modal.reason": "Grund",
@@ -2912,12 +2917,12 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.modal.error.create": "Anlegen fehlgeschlagen.",
"admin.rules.modal.error.resolve": "Zuordnung fehlgeschlagen.",
"admin.rules.edit.title": "Regel bearbeiten — Paliad",
"admin.rules.edit.heading.loading": "Regel laden…",
"admin.rules.edit.breadcrumb": "← Regeln verwalten",
"admin.rules.edit.error.bad_id": "Ungültige Regel-ID in der URL.",
"admin.rules.edit.error.not_found": "Regel nicht gefunden.",
"admin.rules.edit.error.load": "Konnte Regel nicht laden.",
"admin.rules.edit.title": "Verfahrensschritt bearbeiten — Paliad",
"admin.rules.edit.heading.loading": "Verfahrensschritt laden…",
"admin.rules.edit.breadcrumb": "← Verfahrensschritte verwalten",
"admin.rules.edit.error.bad_id": "Ungültige Verfahrensschritt-ID in der URL.",
"admin.rules.edit.error.not_found": "Verfahrensschritt nicht gefunden.",
"admin.rules.edit.error.load": "Konnte Verfahrensschritt nicht laden.",
"admin.rules.edit.section.identity": "Identität",
"admin.rules.edit.section.proceeding": "Verfahren & Trigger",
@@ -2930,14 +2935,14 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.edit.field.name": "Name (DE)",
"admin.rules.edit.field.name_en": "Name (EN)",
"admin.rules.edit.field.description": "Beschreibung",
"admin.rules.edit.field.submission_code": "Submission Code / Einreichung-Kennung",
"admin.rules.edit.field.submission_code": "Code (Verfahrensschritt-Identifikator)",
"admin.rules.edit.field.rule_code": "Rechtsgrundlage (Kurzform)",
"admin.rules.edit.field.legal_source": "Rechtsgrundlage (Langform)",
"admin.rules.edit.field.proceeding": "Verfahrenstyp",
"admin.rules.edit.field.proceeding.none": "—",
"admin.rules.edit.field.trigger": "Trigger-Ereignis",
"admin.rules.edit.field.trigger.none": "—",
"admin.rules.edit.field.parent": "Parent-Regel (UUID)",
"admin.rules.edit.field.parent": "Übergeordneter Verfahrensschritt (UUID)",
"admin.rules.edit.field.concept": "Konzept (UUID)",
"admin.rules.edit.field.sequence_order": "Reihenfolge",
"admin.rules.edit.field.duration_value": "Dauer",
@@ -2949,7 +2954,7 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.edit.field.alt_rule_code": "Alt-Rule-Code",
"admin.rules.edit.field.anchor_alt": "Alt-Anchor",
"admin.rules.edit.field.primary_party": "Primäre Partei",
"admin.rules.edit.field.event_type": "Event-Typ (frei)",
"admin.rules.edit.field.event_type": "Art des Verfahrensschritts (filing / hearing / decision / order)",
"admin.rules.edit.field.deadline_notes": "Hinweise (DE)",
"admin.rules.edit.field.deadline_notes_en": "Hinweise (EN)",
"admin.rules.edit.field.priority": "Priorität",
@@ -3060,6 +3065,21 @@ const translations: Record<Lang, Record<string, string>> = {
"date_range.custom.invalid": "Bis-Datum muss nach Von-Datum liegen.",
"date_range.custom.invalid_format": "Datum nicht erkannt (Format JJJJ-MM-TT).",
"date_range.custom.invalid_missing": "Bitte beide Datumsfelder ausfüllen.",
// t-paliad-262 Slice A — canonical `procedural_event` i18n contract.
// The values are identical to the legacy `admin.rules.*` keys above —
// these aliases let .tsx files rebind in Slice B (B.5) without
// touching DE/EN strings then. Adding/changing values? Update BOTH
// sides.
"admin.procedural_events.list.title": "Verfahrensschritte verwalten — Paliad",
"admin.procedural_events.list.heading": "Verfahrensschritte verwalten",
"admin.procedural_events.list.new": "+ Neuer Verfahrensschritt",
"admin.procedural_events.col.code": "Code (Verfahrensschritt)",
"admin.procedural_events.edit.title": "Verfahrensschritt bearbeiten — Paliad",
"admin.procedural_events.edit.breadcrumb":"← Verfahrensschritte verwalten",
"admin.procedural_events.edit.field.code": "Code (Verfahrensschritt-Identifikator)",
"admin.procedural_events.edit.field.event_kind": "Art des Verfahrensschritts (filing / hearing / decision / order)",
"admin.procedural_events.edit.field.parent": "Übergeordneter Verfahrensschritt (UUID)",
},
en: {
@@ -5854,21 +5874,22 @@ const translations: Record<Lang, Record<string, string>> = {
"views.bar.save.error.network": "Network error — please retry.",
// t-paliad-192 Slice 11b — Admin rule-editor UI.
"nav.admin.rules": "Manage Rules",
"nav.admin.rules_export": "Rule Migrations",
"admin.card.rules.title": "Manage Rules",
"admin.card.rules.desc": "Author, edit and publish deadline rules. Audit log, preview, migration export.",
// t-paliad-262 Slice A — "Rule" relabelled as "Procedural event".
"nav.admin.rules": "Manage procedural events",
"nav.admin.rules_export": "Procedural-event migrations",
"admin.card.rules.title": "Manage procedural events",
"admin.card.rules.desc": "Author, edit and publish procedural-event templates. Audit log, preview, migration export.",
"admin.rules.list.title": "Manage Rules — Paliad",
"admin.rules.list.heading": "Manage Rules",
"admin.rules.list.subtitle": "Author, edit and publish deadline rules. Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ New Rule",
"admin.rules.list.title": "Manage procedural events — Paliad",
"admin.rules.list.heading": "Manage procedural events",
"admin.rules.list.subtitle": "Author, edit and publish procedural events (filings, hearings, decisions, …). Lifecycle: draft → published → archived.",
"admin.rules.list.new": "+ New procedural event",
"admin.rules.list.export": "Export migrations",
"admin.rules.tab.rules": "Rules",
"admin.rules.tab.orphans": "Orphans",
"admin.rules.loading": "Loading…",
"admin.rules.empty": "No rules for the chosen filters.",
"admin.rules.error.load": "Could not load rules.",
"admin.rules.empty": "No procedural events for the chosen filters.",
"admin.rules.error.load": "Could not load procedural events.",
"admin.rules.filter.proceeding": "Proceeding type",
"admin.rules.filter.proceeding.any": "Any",
@@ -5879,7 +5900,7 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.filter.search": "Search",
"admin.rules.filter.search.placeholder": "Name, submission code, legal citation…",
"admin.rules.col.submission_code": "Submission code",
"admin.rules.col.submission_code": "Code (procedural event)",
"admin.rules.col.legal_citation": "Legal citation",
"admin.rules.col.name": "Name",
"admin.rules.col.proceeding": "Proceeding type",
@@ -5909,8 +5930,8 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.orphans.reason.manual_unbound": "Manually unbound",
"admin.rules.orphans.resolved": "Orphan resolved.",
"admin.rules.modal.new.title": "Create new rule",
"admin.rules.modal.new.body": "A new rule will be created as a draft. Please supply a reason (≥10 chars) — recorded in the audit log and exported into the migration file.",
"admin.rules.modal.new.title": "Create new procedural event",
"admin.rules.modal.new.body": "A new procedural event will be created as a draft. Please supply a reason (≥10 chars) — recorded in the audit log and exported into the migration file.",
"admin.rules.modal.resolve.title": "Resolve orphan",
"admin.rules.modal.resolve.body": "Please supply a reason (≥10 chars). The rule binding is persisted immediately on the deadline.",
"admin.rules.modal.reason": "Reason",
@@ -5925,12 +5946,12 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.modal.error.create": "Creation failed.",
"admin.rules.modal.error.resolve": "Resolution failed.",
"admin.rules.edit.title": "Edit Rule — Paliad",
"admin.rules.edit.heading.loading": "Loading rule…",
"admin.rules.edit.breadcrumb": "← Manage Rules",
"admin.rules.edit.error.bad_id": "Invalid rule id in URL.",
"admin.rules.edit.error.not_found": "Rule not found.",
"admin.rules.edit.error.load": "Could not load rule.",
"admin.rules.edit.title": "Edit procedural event — Paliad",
"admin.rules.edit.heading.loading": "Loading procedural event…",
"admin.rules.edit.breadcrumb": "← Manage procedural events",
"admin.rules.edit.error.bad_id": "Invalid procedural-event id in URL.",
"admin.rules.edit.error.not_found": "Procedural event not found.",
"admin.rules.edit.error.load": "Could not load procedural event.",
"admin.rules.edit.section.identity": "Identity",
"admin.rules.edit.section.proceeding": "Proceeding & Trigger",
@@ -5943,14 +5964,14 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.edit.field.name": "Name (DE)",
"admin.rules.edit.field.name_en": "Name (EN)",
"admin.rules.edit.field.description": "Description",
"admin.rules.edit.field.submission_code": "Submission code",
"admin.rules.edit.field.submission_code": "Code (procedural-event identifier)",
"admin.rules.edit.field.rule_code": "Legal citation (short form)",
"admin.rules.edit.field.legal_source": "Legal citation (long form)",
"admin.rules.edit.field.proceeding": "Proceeding type",
"admin.rules.edit.field.proceeding.none": "—",
"admin.rules.edit.field.trigger": "Trigger event",
"admin.rules.edit.field.trigger.none": "—",
"admin.rules.edit.field.parent": "Parent rule (UUID)",
"admin.rules.edit.field.parent": "Parent procedural event (UUID)",
"admin.rules.edit.field.concept": "Concept (UUID)",
"admin.rules.edit.field.sequence_order": "Order",
"admin.rules.edit.field.duration_value": "Duration",
@@ -5962,7 +5983,7 @@ const translations: Record<Lang, Record<string, string>> = {
"admin.rules.edit.field.alt_rule_code": "Alt rule code",
"admin.rules.edit.field.anchor_alt": "Alt anchor",
"admin.rules.edit.field.primary_party": "Primary party",
"admin.rules.edit.field.event_type": "Event type (free)",
"admin.rules.edit.field.event_type": "Procedural-event kind (filing / hearing / decision / order)",
"admin.rules.edit.field.deadline_notes": "Notes (DE)",
"admin.rules.edit.field.deadline_notes_en": "Notes (EN)",
"admin.rules.edit.field.priority": "Priority",
@@ -6070,6 +6091,19 @@ const translations: Record<Lang, Record<string, string>> = {
"date_range.custom.invalid": "End date must be strictly after start date.",
"date_range.custom.invalid_format": "Date not recognised (format YYYY-MM-DD).",
"date_range.custom.invalid_missing": "Please fill in both date fields.",
// t-paliad-262 Slice A — canonical `procedural_event` i18n contract.
// Mirrors the DE block; values identical to the legacy
// `admin.rules.*` keys. Adding/changing values? Update BOTH sides.
"admin.procedural_events.list.title": "Manage procedural events — Paliad",
"admin.procedural_events.list.heading": "Manage procedural events",
"admin.procedural_events.list.new": "+ New procedural event",
"admin.procedural_events.col.code": "Code (procedural event)",
"admin.procedural_events.edit.title": "Edit procedural event — Paliad",
"admin.procedural_events.edit.breadcrumb":"← Manage procedural events",
"admin.procedural_events.edit.field.code": "Code (procedural-event identifier)",
"admin.procedural_events.edit.field.event_kind": "Procedural-event kind (filing / hearing / decision / order)",
"admin.procedural_events.edit.field.parent": "Parent procedural event (UUID)",
},
};

View File

@@ -155,14 +155,29 @@ const VARIABLE_LABELS: Record<string, VariableLabel> = {
"parties.defendant.representative":{ de: "Beklagten-Vertreter", en: "Defendant representative" },
"parties.other.name": { de: "Weitere Partei", en: "Other party" },
"parties.other.representative": { de: "Weitere-Partei-Vertreter", en: "Other party representative" },
"rule.submission_code": { de: "Schriftsatz-Code", en: "Submission code" },
"rule.name": { de: "Schriftsatz", en: "Submission" },
"rule.name_de": { de: "Schriftsatz (DE)", en: "Submission (DE)" },
"rule.name_en": { de: "Schriftsatz (EN)", en: "Submission (EN)" },
"rule.legal_source": { de: "Rechtsgrundlage (Code)", en: "Legal source (code)" },
"rule.legal_source_pretty": { de: "Rechtsgrundlage", en: "Legal source" },
"rule.primary_party": { de: "Partei (typisch)", en: "Primary party" },
"rule.event_type": { de: "Schriftsatz-Typ", en: "Event type" },
// Procedural-event namespace (t-paliad-262 Slice A, design doc
// docs/design-procedural-events-model-2026-05-25.md). The canonical
// placeholder names are below; the `rule.*` aliases that follow are
// @deprecated but kept forever per m's Q7 lock — existing Word
// templates and saved drafts authored with the old names keep
// merging identically.
"procedural_event.code": { de: "Code (Verfahrensschritt)", en: "Code (procedural event)" },
"procedural_event.name": { de: "Verfahrensschritt", en: "Procedural event" },
"procedural_event.name_de": { de: "Verfahrensschritt (DE)", en: "Procedural event (DE)" },
"procedural_event.name_en": { de: "Verfahrensschritt (EN)", en: "Procedural event (EN)" },
"procedural_event.legal_source": { de: "Rechtsgrundlage (Code)", en: "Legal source (code)" },
"procedural_event.legal_source_pretty":{ de: "Rechtsgrundlage", en: "Legal source" },
"procedural_event.primary_party": { de: "Partei (typisch)", en: "Primary party" },
"procedural_event.event_kind": { de: "Art des Verfahrensschritts", en: "Procedural-event kind" },
// Legacy aliases — @deprecated, kept forever (m/paliad#93 Q7).
"rule.submission_code": { de: "Schriftsatz-Code (legacy)", en: "Submission code (legacy)" },
"rule.name": { de: "Schriftsatz (legacy)", en: "Submission (legacy)" },
"rule.name_de": { de: "Schriftsatz (DE, legacy)", en: "Submission (DE, legacy)" },
"rule.name_en": { de: "Schriftsatz (EN, legacy)", en: "Submission (EN, legacy)" },
"rule.legal_source": { de: "Rechtsgrundlage (Code, legacy)", en: "Legal source (code, legacy)" },
"rule.legal_source_pretty": { de: "Rechtsgrundlage (legacy)", en: "Legal source (legacy)" },
"rule.primary_party": { de: "Partei (typisch, legacy)", en: "Primary party (legacy)" },
"rule.event_type": { de: "Schriftsatz-Typ (legacy)", en: "Event type (legacy)" },
"deadline.due_date": { de: "Frist (ISO)", en: "Deadline (ISO)" },
"deadline.due_date_long_de": { de: "Frist (DE lang)", en: "Deadline (DE long)" },
"deadline.due_date_long_en": { de: "Frist (EN lang)", en: "Deadline (EN long)" },
@@ -174,14 +189,14 @@ const VARIABLE_LABELS: Record<string, VariableLabel> = {
const VARIABLE_GROUPS: VariableGroup[] = [
{
id: "rule",
label: { de: "Schriftsatz", en: "Submission" },
id: "procedural_event",
label: { de: "Verfahrensschritt", en: "Procedural event" },
keys: [
"rule.name",
"rule.legal_source_pretty",
"rule.primary_party",
"rule.event_type",
"rule.submission_code",
"procedural_event.name",
"procedural_event.legal_source_pretty",
"procedural_event.primary_party",
"procedural_event.event_kind",
"procedural_event.code",
],
},
{

View File

@@ -290,6 +290,15 @@ export type I18nKey =
| "admin.partner_units.new.heading"
| "admin.partner_units.subtitle"
| "admin.partner_units.title"
| "admin.procedural_events.col.code"
| "admin.procedural_events.edit.breadcrumb"
| "admin.procedural_events.edit.field.code"
| "admin.procedural_events.edit.field.event_kind"
| "admin.procedural_events.edit.field.parent"
| "admin.procedural_events.edit.title"
| "admin.procedural_events.list.heading"
| "admin.procedural_events.list.new"
| "admin.procedural_events.list.title"
| "admin.rules.col.legal_citation"
| "admin.rules.col.lifecycle"
| "admin.rules.col.modified"

View File

@@ -6,17 +6,28 @@ package services
//
// Variables span six namespaces:
//
// firm.* process-wide (branding.Name)
// user.* caller's user row
// today.* server time in Europe/Berlin, locale-aware
// project.* paliad.projects + joined proceeding type
// parties.* paliad.parties grouped by role
// rule.* paliad.deadline_rules row keyed by submission_code
// deadline.* next open paliad.deadlines row for (project, rule), if any
// firm.* process-wide (branding.Name)
// user.* caller's user row
// today.* server time in Europe/Berlin, locale-aware
// project.* paliad.projects + joined proceeding type
// parties.* paliad.parties grouped by role
// procedural_event.* paliad.deadline_rules row keyed by submission_code
// — the "what kind of step in the proceeding"
// identity (Schriftsatz, Anhörung, Entscheidung,
// …). See docs/design-procedural-events-model-
// 2026-05-25.md (t-paliad-262 Slice A).
// rule.* legacy alias for procedural_event.*; emitted
// unconditionally for backward compatibility
// with Word templates and saved drafts authored
// before the rename. @deprecated — new templates
// should use the procedural_event.* form.
// deadline.* next open paliad.deadlines row for
// (project, procedural_event), if any
//
// Locale handling: every long-form date string is computed in both DE
// and EN; the renderer picks based on the user's lang preference. The
// rule pretty-printer (legalSourcePretty) also has DE/EN variants.
// procedural-event pretty-printer (legalSourcePretty) also has DE/EN
// variants.
//
// Visibility: caller passes userID; ProjectService.GetByID enforces
// paliad.can_see_project — unauthorised callers get the standard
@@ -173,9 +184,12 @@ func (s *SubmissionVarsService) Build(ctx context.Context, in SubmissionVarsCont
return out, nil
}
// loadPublishedRule fetches the deadline_rule that owns the given
// submission_code. Restricts to lifecycle_state='published' so drafts
// never end up shaping a real submission.
// loadPublishedRule fetches the published procedural-event template
// (paliad.deadline_rules row) keyed by submission_code. Restricts to
// lifecycle_state='published' so drafts never end up shaping a real
// submission. Function name retained for Slice A (prose-only); Slice
// B renames it to loadPublishedProceduralEvent when the Go type is
// renamed (t-paliad-262 §6).
func (s *SubmissionVarsService) loadPublishedRule(ctx context.Context, submissionCode string) (*models.DeadlineRule, error) {
if submissionCode == "" {
return nil, ErrSubmissionRuleNotFound
@@ -346,21 +360,55 @@ func addPartyVars(bag PlaceholderMap, parties []models.Party) {
}
}
// addRuleVars populates rule.* — submission_code, name(_en),
// legal_source (+ pretty form), primary_party, event_type.
// addRuleVars populates the procedural-event variable namespace —
// code, name(_en), legal_source (+ pretty form), primary_party, kind.
//
// Two key prefixes are emitted for every value:
//
// - procedural_event.* — canonical name (t-paliad-262 Slice A,
// design docs/design-procedural-events-model-2026-05-25.md).
// - rule.* — legacy alias kept forever (m's call,
// issue m/paliad#93 Q7); existing Word templates and saved
// submission_drafts authored before the rename keep working.
//
// `procedural_event.event_kind` is the canonical key for the
// procedural-event kind (filing|reply|hearing|decision|order). The
// legacy `rule.event_type` alias holds the same string. The column
// itself stays named `event_type` on `paliad.deadline_rules` — Slice
// A is prose-only; the column-level rename to `event_kind` is Slice B.
//
// Function name stays `addRuleVars` to avoid coupling Slice A to the
// Go-type rename which is Slice B (B.5 sub-slice).
func addRuleVars(bag PlaceholderMap, r *models.DeadlineRule, lang string) {
bag["rule.submission_code"] = derefString(r.SubmissionCode)
code := derefString(r.SubmissionCode)
var localizedName string
if strings.EqualFold(lang, "en") {
bag["rule.name"] = r.NameEN
localizedName = r.NameEN
} else {
bag["rule.name"] = r.Name
localizedName = r.Name
}
legalSource := derefString(r.LegalSource)
legalSourcePrettyVal := legalSourcePretty(legalSource, lang)
primaryParty := derefString(r.PrimaryParty)
eventKind := derefString(r.EventType)
bag["procedural_event.code"] = code
bag["procedural_event.name"] = localizedName
bag["procedural_event.name_de"] = r.Name
bag["procedural_event.name_en"] = r.NameEN
bag["procedural_event.legal_source"] = legalSource
bag["procedural_event.legal_source_pretty"] = legalSourcePrettyVal
bag["procedural_event.primary_party"] = primaryParty
bag["procedural_event.event_kind"] = eventKind
bag["rule.submission_code"] = code
bag["rule.name"] = localizedName
bag["rule.name_de"] = r.Name
bag["rule.name_en"] = r.NameEN
bag["rule.legal_source"] = derefString(r.LegalSource)
bag["rule.legal_source_pretty"] = legalSourcePretty(derefString(r.LegalSource), lang)
bag["rule.primary_party"] = derefString(r.PrimaryParty)
bag["rule.event_type"] = derefString(r.EventType)
bag["rule.legal_source"] = legalSource
bag["rule.legal_source_pretty"] = legalSourcePrettyVal
bag["rule.primary_party"] = primaryParty
bag["rule.event_type"] = eventKind
}
// addDeadlineVars populates deadline.* from the next pending row. When

View File

@@ -0,0 +1,153 @@
package services
// Regression test for the procedural-event placeholder aliases
// (t-paliad-262 Slice A, m/paliad#93 Q7).
//
// The variable bag emits TWO key prefixes for the procedural-event
// namespace:
//
// - procedural_event.* (canonical, post-rename)
// - rule.* (legacy, @deprecated)
//
// m's lock: keep the legacy aliases forever so lawyer-authored Word
// templates and existing paliad.submission_drafts rows that already
// contain `{{rule.X}}` keep merging correctly.
//
// This test pins the contract: every (canonical, legacy) pair must
// resolve to the same string in the placeholder map, for every value
// of (lang, present-vs-NULL columns). Removing the legacy aliases —
// or letting them drift in value from the canonical — must light up
// here BEFORE the change can land in main.
import (
"strings"
"testing"
"github.com/google/uuid"
"mgit.msbls.de/m/paliad/internal/models"
)
func TestAddRuleVars_CanonicalAndLegacyAliasesMatch(t *testing.T) {
t.Parallel()
// Pairs are (canonical key, legacy key). Order matters only for
// the assertion message — the test checks string equality both
// ways round.
pairs := []struct {
canonical string
legacy string
}{
{"procedural_event.code", "rule.submission_code"},
{"procedural_event.name", "rule.name"},
{"procedural_event.name_de", "rule.name_de"},
{"procedural_event.name_en", "rule.name_en"},
{"procedural_event.legal_source", "rule.legal_source"},
{"procedural_event.legal_source_pretty", "rule.legal_source_pretty"},
{"procedural_event.primary_party", "rule.primary_party"},
{"procedural_event.event_kind", "rule.event_type"},
}
// Build a fully-populated rule row. Every nullable column has a
// distinct non-empty value so missing-value bugs (e.g. the legacy
// key copying "" while the canonical key copies the real value)
// would surface.
code := "dpma.appeal.bgh.begruendung"
desc := "Rechtsbeschwerdebegründung — § 102 PatG"
party := "both"
kind := "filing"
legal := "DE.PatG.102"
ruleCode := "§ 102 PatG"
rule := &models.DeadlineRule{
ID: uuid.New(),
SubmissionCode: &code,
Name: "Rechtsbeschwerdebegründung",
NameEN: "Appeal brief",
Description: &desc,
PrimaryParty: &party,
EventType: &kind,
LegalSource: &legal,
RuleCode: &ruleCode,
}
for _, lang := range []string{"de", "en"} {
t.Run(lang, func(t *testing.T) {
t.Parallel()
bag := PlaceholderMap{}
addRuleVars(bag, rule, lang)
for _, p := range pairs {
canonicalVal, canonicalOK := bag[p.canonical]
legacyVal, legacyOK := bag[p.legacy]
if !canonicalOK {
t.Errorf("canonical key %q missing from bag (lang=%s); "+
"Slice A must emit both forms", p.canonical, lang)
}
if !legacyOK {
t.Errorf("legacy alias %q missing from bag (lang=%s); "+
"removing legacy aliases would break existing Word "+
"templates that paliad doesn't see — keep the "+
"emission per m/paliad#93 Q7", p.legacy, lang)
}
if canonicalVal != legacyVal {
t.Errorf("alias drift: %q=%q vs %q=%q (lang=%s)",
p.canonical, canonicalVal,
p.legacy, legacyVal, lang)
}
}
// Sanity: the localized name actually localizes (the
// canonical and legacy `name` keys depend on lang). If
// this fails the loop above wouldn't catch it (both keys
// would agree on the wrong language).
localized := bag["procedural_event.name"]
if strings.EqualFold(lang, "en") && localized != rule.NameEN {
t.Errorf("expected EN localized name=%q, got %q",
rule.NameEN, localized)
}
if strings.EqualFold(lang, "de") && localized != rule.Name {
t.Errorf("expected DE localized name=%q, got %q",
rule.Name, localized)
}
})
}
}
func TestAddRuleVars_NullableFieldsEmitEmptyOnBothPrefixes(t *testing.T) {
t.Parallel()
// A minimal rule with every optional column NULL. The bag must
// still emit every canonical + legacy key — with the empty
// string — so downstream merging produces the standard
// "[KEIN WERT: ...]" marker rather than a broken template.
rule := &models.DeadlineRule{
ID: uuid.New(),
Name: "Generic step",
NameEN: "Generic step",
}
bag := PlaceholderMap{}
addRuleVars(bag, rule, "de")
mustHave := []string{
"procedural_event.code", "rule.submission_code",
"procedural_event.legal_source", "rule.legal_source",
"procedural_event.legal_source_pretty", "rule.legal_source_pretty",
"procedural_event.primary_party", "rule.primary_party",
"procedural_event.event_kind", "rule.event_type",
}
for _, key := range mustHave {
val, ok := bag[key]
if !ok {
t.Errorf("key %q missing from bag even with NULL source column; "+
"derefString must materialize the empty string so the "+
"merger sees the variable and renders the missing-value "+
"marker", key)
}
if val != "" {
t.Errorf("key %q = %q, want \"\" (source column was NULL)", key, val)
}
}
}