feat(t-paliad-131): Phase B1 — UPC counterclaim cross-flows

Closes m's primary complaint: today's `with_ccr` flag on UPC_INF only
swaps the Replik / Duplik durations. Per UPC RoP R.29 the with-CCR flow
ALSO adds 5–7 new submissions across the claimant / defendant exchange.
Same gap on UPC_REV: Application to amend (R.49.2.a → R.55 = R.32 m.m.)
and Counterclaim for infringement (R.49.2.b → R.50, R.56 cycle) were
entirely missing.

UPC_INF gets a nested `with_amend` flag under `with_ccr` (R.30 amend
is only available with a CCR). UPC_REV gets two parallel independent
flags `with_amend` + `with_cci`; both can be on. Citations verified
against data.laws_contents (youpcdb, UPCRoP).

Migration 041 (waved INSERTs because each subsequent rule references
the prior wave's parent_id):
- Wave 0: 11 new concept rows (counterclaim-for-revocation,
  defence-to-counterclaim-for-revocation, defence-to-application-to-amend,
  reply-to-defence-to-counterclaim-for-revocation,
  reply-to-defence-to-application-to-amend,
  rejoinder-on-reply-to-defence-to-ccr, rejoinder-on-reply-to-amend,
  counterclaim-for-infringement, defence-to-counterclaim-for-infringement,
  reply-to-defence-to-counterclaim-for-infringement,
  rejoinder-on-counterclaim-for-infringement). counterclaim-for-revocation
  also seeded for the search bar even though its rule lives implicitly
  in inf.sod (the with_ccr flag captures it).
- UPC_INF + UPC_REV sequence_orders renumbered to leave gaps (10/20/30…)
  so new cross-flow rows interleave chronologically with the backbone.
- 7 new UPC_INF rules: inf.def_to_ccr (R.29.a), inf.app_to_amend (R.30.1),
  inf.def_to_amend (R.32.1), inf.reply_def_ccr (R.29.d),
  inf.reply_def_amd (R.32.3), inf.rejoin_reply_ccr (R.29.e),
  inf.rejoin_amd (R.32.3).
- 8 new UPC_REV rules: rev.app_to_amend (R.49.2.a), rev.def_to_amend
  (R.43.3), rev.reply_def_amd (R.32.3 m.m.), rev.rejoin_amd (R.32.3 m.m.),
  rev.cc_inf (R.49.2.b), rev.def_cci (R.56.1), rev.reply_def_cci (R.56.3),
  rev.rejoin_cci (R.56.4).

Calculator (services/fristenrechner.go):
- Zero-duration rules now split into 4 buckets, not 2:
    1. parent=nil + non-court → IsRootEvent (existing)
    2. parent=nil + court     → IsCourtSet (existing, e.g. inf.oral when stand-alone)
    3. parent set + court     → IsCourtSet (existing, waypoints)
    4. parent set + non-court → "filed-with-parent" — inherit parent's
       date. NEW. Used by rev.app_to_amend / rev.cc_inf which per
       R.49(2) are filed AS PART OF the Defence to revocation.
- AnchorOverrides on a zero-duration rule short-circuits to the user's
  date, propagating downstream as before.

Frontend:
- New checkboxes inf-amend-flag (UPC_INF, nested under ccr-flag),
  rev-amend-flag, rev-cci-flag (UPC_REV). Visibility per proceeding
  type; inf-amend disabled until ccr is on (R.30 dependency).
- Three new i18n keys (DE+EN). Small CSS for nested-checkbox indent
  and disabled-state colour.

Live-verified via curl on paliad.de against tester@hlc.de:
  UPC_INF + with_ccr+with_amend, trigger 2026-05-04 → all 7 new rules
  render at correct dates (R.29.a 2mo, R.30.1 2mo, R.32.1 2mo from
  app_to_amend, R.29.d 2mo from def_to_ccr, R.32.3 1mo, R.29.e 1mo,
  R.32.3 1mo).
  UPC_REV + with_amend+with_cci → rev.app_to_amend / rev.cc_inf show
  rev.defence's date (filed-with-parent), R.43.3 2mo / R.56.1 2mo /
  R.32.3 + R.56.3 1mo / R.32.3 + R.56.4 1mo all line up.
This commit is contained in:
m
2026-05-05 01:25:03 +02:00
parent 258ebb8508
commit cc68ab2873
8 changed files with 516 additions and 31 deletions

View File

@@ -202,10 +202,24 @@ async function calculate() {
const priorityInput = document.getElementById("priority-date") as HTMLInputElement | null;
const priorityDate = selectedType === "EP_GRANT" && priorityInput?.value ? priorityInput.value : "";
// Flags — UPC_INF surfaces "Mit Widerklage auf Nichtigkeit" toggle.
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
// Flags — three proceeding-specific checkboxes:
// UPC_INF: with_ccr (always available); with_amend (nested under
// with_ccr — R.30 application is only available with a CCR).
// UPC_REV: with_amend (R.49.2.a) and with_cci (R.49.2.b) as two
// independent gates; both can be on simultaneously.
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
const infAmendFlag = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
const revAmendFlag = document.getElementById("rev-amend-flag") as HTMLInputElement | null;
const revCciFlag = document.getElementById("rev-cci-flag") as HTMLInputElement | null;
const flags: string[] = [];
if (selectedType === "UPC_INF" && ccrFlag?.checked) flags.push("with_ccr");
if (selectedType === "UPC_INF") {
if (ccrFlag?.checked) flags.push("with_ccr");
if (ccrFlag?.checked && infAmendFlag?.checked) flags.push("with_amend");
}
if (selectedType === "UPC_REV") {
if (revAmendFlag?.checked) flags.push("with_amend");
if (revCciFlag?.checked) flags.push("with_cci");
}
// Forward any user-set per-rule date overrides so downstream rules
// re-anchor off them. Empty map → omitted from the payload.
@@ -686,16 +700,41 @@ function selectProceeding(btn: HTMLButtonElement) {
const name = btn.querySelector("strong")?.textContent || "";
document.getElementById("trigger-event")!.textContent = name;
// Conditional inputs: priority date for EP_GRANT, CCR toggle for UPC_INF.
// Conditional inputs:
// priority-date → EP_GRANT
// ccr-flag → UPC_INF only
// inf-amend-flag → UPC_INF only, but disabled until ccr-flag is on
// (R.30 amend only available with a CCR)
// rev-amend-flag → UPC_REV only
// rev-cci-flag → UPC_REV only
const priorityRow = document.getElementById("priority-date-row");
if (priorityRow) priorityRow.style.display = selectedType === "EP_GRANT" ? "" : "none";
const ccrRow = document.getElementById("ccr-flag-row");
if (ccrRow) ccrRow.style.display = selectedType === "UPC_INF" ? "" : "none";
const infAmendRow = document.getElementById("inf-amend-flag-row");
if (infAmendRow) infAmendRow.style.display = selectedType === "UPC_INF" ? "" : "none";
const revAmendRow = document.getElementById("rev-amend-flag-row");
if (revAmendRow) revAmendRow.style.display = selectedType === "UPC_REV" ? "" : "none";
const revCciRow = document.getElementById("rev-cci-flag-row");
if (revCciRow) revCciRow.style.display = selectedType === "UPC_REV" ? "" : "none";
syncInfAmendEnabled();
showStep(2);
scheduleProcCalc(0);
}
// inf-amend-flag is only meaningful when ccr-flag is on (R.30 application
// is filed within the Defence to CCR). When ccr-flag flips off, also
// untick inf-amend-flag so the calc payload stays coherent.
function syncInfAmendEnabled() {
const ccr = document.getElementById("ccr-flag") as HTMLInputElement | null;
const infAmend = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
if (!ccr || !infAmend) return;
infAmend.disabled = !ccr.checked;
if (!ccr.checked) infAmend.checked = false;
}
// View toggle wiring. Persist the choice in `?view=…` so reload / share-link
// restores the same layout.
function initViewToggle() {
@@ -755,7 +794,17 @@ document.addEventListener("DOMContentLoaded", () => {
}
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
if (ccrFlag) ccrFlag.addEventListener("change", () => scheduleProcCalc(0));
if (ccrFlag) ccrFlag.addEventListener("change", () => {
syncInfAmendEnabled();
scheduleProcCalc(0);
});
const infAmendFlag = document.getElementById("inf-amend-flag") as HTMLInputElement | null;
if (infAmendFlag) infAmendFlag.addEventListener("change", () => scheduleProcCalc(0));
const revAmendFlag = document.getElementById("rev-amend-flag") as HTMLInputElement | null;
if (revAmendFlag) revAmendFlag.addEventListener("change", () => scheduleProcCalc(0));
const revCciFlag = document.getElementById("rev-cci-flag") as HTMLInputElement | null;
if (revCciFlag) revCciFlag.addEventListener("change", () => scheduleProcCalc(0));
// Click-to-edit on timeline / column dates: open an inline date input
// and persist the user's choice as an anchor override so downstream

View File

@@ -208,6 +208,9 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.trigger.label": "Ausgangsdatum",
"deadlines.priority.date": "Priorit\u00e4tstag (optional):",
"deadlines.flag.ccr": "Mit Widerklage auf Nichtigkeit",
"deadlines.flag.inf_amend": "Mit Antrag auf Patentänderung (R.30)",
"deadlines.flag.rev_amend": "Mit Antrag auf Patentänderung (R.49.2.a)",
"deadlines.flag.rev_cci": "Mit Verletzungswiderklage (R.49.2.b)",
"deadlines.calculate": "Fristen berechnen",
"deadlines.print": "Drucken",
"deadlines.reset": "\u2190 Neu berechnen",
@@ -1728,6 +1731,9 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.trigger.label": "Trigger date",
"deadlines.priority.date": "Priority date (optional):",
"deadlines.flag.ccr": "Counterclaim for revocation filed",
"deadlines.flag.inf_amend": "Application to amend the patent filed (R.30)",
"deadlines.flag.rev_amend": "Application to amend the patent filed (R.49.2.a)",
"deadlines.flag.rev_cci": "Counterclaim for infringement filed (R.49.2.b)",
"deadlines.calculate": "Calculate Deadlines",
"deadlines.print": "Print",
"deadlines.reset": "\u2190 Start Over",

View File

@@ -128,6 +128,24 @@ export function renderFristenrechner(): string {
<span data-i18n="deadlines.flag.ccr">Mit Widerklage auf Nichtigkeit</span>
</label>
</div>
<div className="date-field-row date-field-row--nested" id="inf-amend-flag-row" style="display:none">
<label className="date-label">
<input type="checkbox" id="inf-amend-flag" />
<span data-i18n="deadlines.flag.inf_amend">Mit Antrag auf Patent&auml;nderung (R.30)</span>
</label>
</div>
<div className="date-field-row" id="rev-amend-flag-row" style="display:none">
<label className="date-label">
<input type="checkbox" id="rev-amend-flag" />
<span data-i18n="deadlines.flag.rev_amend">Mit Antrag auf Patent&auml;nderung (R.49.2.a)</span>
</label>
</div>
<div className="date-field-row" id="rev-cci-flag-row" style="display:none">
<label className="date-label">
<input type="checkbox" id="rev-cci-flag" />
<span data-i18n="deadlines.flag.rev_cci">Mit Verletzungswiderklage (R.49.2.b)</span>
</label>
</div>
<button type="button" id="calculate-btn" className="calculate-btn" data-i18n="deadlines.calculate">
Fristen berechnen
</button>

View File

@@ -638,6 +638,9 @@ export type I18nKey =
| "deadlines.filter.thisweek"
| "deadlines.filter.today"
| "deadlines.flag.ccr"
| "deadlines.flag.inf_amend"
| "deadlines.flag.rev_amend"
| "deadlines.flag.rev_cci"
| "deadlines.heading"
| "deadlines.kalender.empty"
| "deadlines.kalender.heading"

View File

@@ -1826,6 +1826,16 @@ input[type="range"]::-moz-range-thumb {
gap: 0.75rem;
}
/* Nested checkbox under a parent flag (e.g. UPC_INF inf-amend-flag is
only meaningful with ccr-flag on — indent so the dependency is
visible). */
.date-field-row--nested {
margin-left: 1.5rem;
}
.date-field-row--nested input[type="checkbox"]:disabled + span {
color: var(--color-text-muted);
}
.date-label {
font-size: 0.85rem;
font-weight: 500;

View File

@@ -0,0 +1,60 @@
-- Reverses 041_upc_counterclaim_cross_flows. Removes the new rules first,
-- then the new concepts, then restores the original sequence_order on the
-- existing UPC_INF / UPC_REV rows. Concept rows that are still referenced
-- elsewhere are kept (defensive — concepts may be reused by future
-- migrations).
DELETE FROM paliad.deadline_rules
WHERE proceeding_type_id IN (
SELECT id FROM paliad.proceeding_types WHERE code IN ('UPC_INF','UPC_REV')
)
AND code IN (
'inf.def_to_ccr','inf.app_to_amend','inf.def_to_amend','inf.reply_def_ccr',
'inf.reply_def_amd','inf.rejoin_reply_ccr','inf.rejoin_amd',
'rev.app_to_amend','rev.def_to_amend','rev.reply_def_amd','rev.rejoin_amd',
'rev.cc_inf','rev.def_cci','rev.reply_def_cci','rev.rejoin_cci'
);
DELETE FROM paliad.deadline_concepts
WHERE slug IN (
'counterclaim-for-revocation','defence-to-counterclaim-for-revocation',
'application-to-amend','defence-to-application-to-amend',
'reply-to-defence-to-counterclaim-for-revocation',
'reply-to-defence-to-application-to-amend',
'rejoinder-on-reply-to-defence-to-ccr','rejoinder-on-reply-to-amend',
'counterclaim-for-infringement','defence-to-counterclaim-for-infringement',
'reply-to-defence-to-counterclaim-for-infringement',
'rejoinder-on-counterclaim-for-infringement'
)
AND id NOT IN (SELECT concept_id FROM paliad.deadline_rules WHERE concept_id IS NOT NULL);
WITH proc AS (SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_INF')
UPDATE paliad.deadline_rules dr
SET sequence_order = CASE dr.code
WHEN 'inf.soc' THEN 0
WHEN 'inf.sod' THEN 1
WHEN 'inf.reply' THEN 2
WHEN 'inf.rejoin' THEN 3
WHEN 'inf.interim' THEN 4
WHEN 'inf.oral' THEN 5
WHEN 'inf.decision' THEN 6
WHEN 'inf.cost_app' THEN 7
ELSE dr.sequence_order
END
FROM proc p
WHERE dr.proceeding_type_id = p.id;
WITH proc AS (SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_REV')
UPDATE paliad.deadline_rules dr
SET sequence_order = CASE dr.code
WHEN 'rev.app' THEN 0
WHEN 'rev.defence' THEN 1
WHEN 'rev.reply' THEN 2
WHEN 'rev.rejoin' THEN 3
WHEN 'rev.interim' THEN 4
WHEN 'rev.oral' THEN 5
WHEN 'rev.decision' THEN 6
ELSE dr.sequence_order
END
FROM proc p
WHERE dr.proceeding_type_id = p.id;

View File

@@ -0,0 +1,303 @@
-- t-paliad-131 Phase B1: UPC counterclaim cross-flows.
--
-- Closes m's primary complaint: today's `with_ccr` flag on UPC_INF only
-- swaps the rejoinder duration from 1mo to 2mo. Per R.29 (UPC RoP) the
-- with-CCR flow ALSO adds 57 new submissions across the claimant /
-- defendant exchange:
-- R.29(a) — claimant: Defence to CCR + (opt) Application to amend (R.30)
-- R.29(d) — defendant: Reply to Defence to CCR + (opt) Defence to App to amend
-- R.29(e) — claimant: Rejoinder + (opt) Reply to Defence to amend
-- R.32(1) / R.32(3) — defendant amend cycle: Defence (2mo) / Reply (1mo) / Rejoinder (1mo)
--
-- And on the revocation side (UPC_REV) per R.49(2):
-- (a) Application to amend → R.55 (= R.32 m.m.) cycle
-- (b) Counterclaim for infringement → R.56 cycle
--
-- Citations verified against data.laws_contents (youpcdb, UPCRoP).
--
-- Two new flags on UPC_INF: nested `with_amend` (only meaningful when
-- with_ccr is also set, since R.30 application is only available with
-- a CCR). UPC_REV gets two parallel independent flags: `with_amend`
-- (R.49.2(a)) and `with_cci` (R.49.2(b)). Both can be on simultaneously.
--
-- New rules are inserted in waves because subsequent rules reference
-- prior-wave inserts as their parent_id (a single INSERT statement can't
-- self-reference its rows in the same SELECT).
-- ============================================================================
-- 1. New concept rows
-- ============================================================================
INSERT INTO paliad.deadline_concepts (slug, name_de, name_en, description, aliases, party, category, sort_order) VALUES
('counterclaim-for-revocation', 'Nichtigkeitswiderklage', 'Counterclaim for Revocation', 'Eingeschlossene Widerklage auf Nichtigerklärung des Patents in der Klageerwiderung (UPC R.25).', ARRAY['Nichtigkeitswiderklage', 'Counterclaim for Revocation', 'CCR', 'Widerklage Nichtigkeit'], 'defendant', 'submission', 23),
('defence-to-counterclaim-for-revocation', 'Erwiderung auf Nichtigkeitswiderklage', 'Defence to Counterclaim for Revocation', 'Erwiderung des Klägers auf eine in der Klageerwiderung enthaltene Nichtigkeitswiderklage (R.29.a).', ARRAY['Erwiderung Nichtigkeitswiderklage', 'Defence to CCR', 'Defence to Counterclaim for Revocation'], 'claimant', 'submission', 24),
('defence-to-application-to-amend', 'Erwiderung auf Patentänderungsantrag', 'Defence to Application to Amend', 'Erwiderung der Gegenseite auf den Patentänderungsantrag (R.32.1 / R.43.3).', ARRAY['Erwiderung Patentänderungsantrag', 'Defence to App to amend', 'Defence to Application to Amend'], 'defendant', 'submission', 26),
('reply-to-defence-to-counterclaim-for-revocation', 'Replik auf Erwiderung zur Nichtigkeitswiderklage', 'Reply to Defence to Counterclaim for Revocation', 'Replik des Beklagten/Patentinhabers auf die Erwiderung zur Nichtigkeitswiderklage (R.29.d).', ARRAY['Replik Erwiderung CCR', 'Reply to Defence to CCR'], 'defendant', 'submission', 27),
('reply-to-defence-to-application-to-amend', 'Replik auf Erwiderung zum Patentänderungsantrag', 'Reply to Defence to Application to Amend', 'Replik des Patentinhabers auf die Erwiderung zum Patentänderungsantrag (R.32.3).', ARRAY['Replik Patentänderungsantrag', 'Reply to Defence to amend'], 'claimant', 'submission', 28),
('rejoinder-on-reply-to-defence-to-ccr', 'Duplik auf Replik zur Erwiderung Nichtigkeitsw.', 'Rejoinder to Reply to Defence to Counterclaim for Revocation', 'Duplik des Klägers auf die Replik zur Erwiderung der Nichtigkeitswiderklage (R.29.e).', ARRAY['Duplik Replik CCR', 'Rejoinder Reply to Defence to CCR'], 'claimant', 'submission', 29),
('rejoinder-on-reply-to-amend', 'Duplik auf Replik zum Patentänderungsantrag', 'Rejoinder to Reply on Application to Amend', 'Duplik der Gegenseite auf die Replik zum Patentänderungsantrag (R.32.3).', ARRAY['Duplik Patentänderungsantrag', 'Rejoinder on amend'], 'defendant', 'submission', 30),
('counterclaim-for-infringement', 'Verletzungswiderklage', 'Counterclaim for Infringement', 'Verletzungswiderklage des Patentinhabers innerhalb des Nichtigkeitsverfahrens, eingeschlossen in der Erwiderung zur Nichtigkeitsklage (R.49.2.b).', ARRAY['Verletzungswiderklage', 'Counterclaim for Infringement', 'CCI'], 'defendant', 'submission', 31),
('defence-to-counterclaim-for-infringement', 'Erwiderung auf Verletzungswiderklage', 'Defence to Counterclaim for Infringement', 'Erwiderung des Klägers im Nichtigkeitsverfahren auf die Verletzungswiderklage (R.56.1).', ARRAY['Erwiderung Verletzungswiderklage', 'Defence to CCI'], 'claimant', 'submission', 32),
('reply-to-defence-to-counterclaim-for-infringement','Replik auf Erwiderung zur Verletzungswiderklage', 'Reply to Defence to Counterclaim for Infringement', 'Replik des Patentinhabers auf die Erwiderung zur Verletzungswiderklage (R.56.3).', ARRAY['Replik Verletzungswiderklage', 'Reply to Defence to CCI'], 'defendant', 'submission', 33),
('rejoinder-on-counterclaim-for-infringement', 'Duplik auf Replik zur Erwiderung Verletzungswiderkl.', 'Rejoinder to Reply on Counterclaim for Infringement', 'Duplik des Klägers auf die Replik zur Erwiderung der Verletzungswiderklage (R.56.4).', ARRAY['Duplik Verletzungswiderklage', 'Rejoinder on CCI'], 'claimant', 'submission', 34)
ON CONFLICT (slug) DO UPDATE SET
name_de = EXCLUDED.name_de,
name_en = EXCLUDED.name_en,
description = EXCLUDED.description,
aliases = EXCLUDED.aliases,
party = EXCLUDED.party,
sort_order = EXCLUDED.sort_order,
updated_at = now();
-- ============================================================================
-- 2. Renumber existing UPC_INF / UPC_REV sequence_orders to leave gaps so
-- new rules can interleave chronologically.
-- ============================================================================
WITH proc AS (SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_INF')
UPDATE paliad.deadline_rules dr
SET sequence_order = CASE dr.code
WHEN 'inf.soc' THEN 0
WHEN 'inf.sod' THEN 10
WHEN 'inf.reply' THEN 20
WHEN 'inf.rejoin' THEN 30
WHEN 'inf.interim' THEN 40
WHEN 'inf.oral' THEN 50
WHEN 'inf.decision' THEN 60
WHEN 'inf.cost_app' THEN 70
ELSE dr.sequence_order
END
FROM proc p
WHERE dr.proceeding_type_id = p.id;
WITH proc AS (SELECT id FROM paliad.proceeding_types WHERE code = 'UPC_REV')
UPDATE paliad.deadline_rules dr
SET sequence_order = CASE dr.code
WHEN 'rev.app' THEN 0
WHEN 'rev.defence' THEN 10
WHEN 'rev.reply' THEN 20
WHEN 'rev.rejoin' THEN 30
WHEN 'rev.interim' THEN 50
WHEN 'rev.oral' THEN 60
WHEN 'rev.decision' THEN 70
ELSE dr.sequence_order
END
FROM proc p
WHERE dr.proceeding_type_id = p.id;
-- ============================================================================
-- 3. UPC_INF cross-flow rules — wave 1 (parents pre-existing inf.sod)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('inf.sod', 'inf.def_to_ccr', 'Erwiderung auf Nichtigkeitswiderklage', 'Defence to Counterclaim for Revocation', 'claimant', 'filing', 2, 'months', 'RoP.029.a',
'Innerhalb von 2 Monaten nach Zustellung der Klageerwiderung mit Nichtigkeitswiderklage. Schließt Replik zur Klageerwiderung und ggf. Patentänderungsantrag (R.30) ein.',
'Within 2 months of service of the SoD which includes a CCR. Includes the Reply to SoD and (if applicable) the Application to amend per R.30.',
11, ARRAY['with_ccr']::text[], 'defence-to-counterclaim-for-revocation', 'UPC.RoP.29.a'),
('inf.sod', 'inf.app_to_amend', 'Antrag auf Patentänderung', 'Application to Amend the Patent', 'claimant', 'filing', 2, 'months', 'RoP.030.1',
'Optional in der Erwiderung zur Nichtigkeitswiderklage einzureichen — siehe R.30. Spätere Änderungsanträge nur mit Zustimmung des Gerichts (R.30.2).',
'Optional, lodged together with the Defence to CCR per R.30. Later amendment requests admitted only with leave of the Court (R.30.2).',
12, ARRAY['with_ccr','with_amend']::text[], 'application-to-amend', 'UPC.RoP.30.1')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_INF'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 4. UPC_INF cross-flow rules — wave 2 (parents = wave-1 inserts)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('inf.app_to_amend', 'inf.def_to_amend', 'Erwiderung auf Patentänderungsantrag', 'Defence to Application to Amend', 'defendant', 'filing', 2, 'months', 'RoP.032.1',
'Innerhalb von 2 Monaten nach Zustellung des Patentänderungsantrags.',
'Within 2 months of service of the Application to amend the patent.',
21, ARRAY['with_ccr','with_amend']::text[], 'defence-to-application-to-amend', 'UPC.RoP.32.1'),
('inf.def_to_ccr', 'inf.reply_def_ccr', 'Replik auf Erwiderung zur Nichtigkeitswiderklage', 'Reply to Defence to Counterclaim for Revocation', 'defendant', 'filing', 2, 'months', 'RoP.029.d',
'Innerhalb von 2 Monaten nach Zustellung der Erwiderung zur Nichtigkeitswiderklage. Schließt Duplik zur Replik auf SoD und ggf. Erwiderung zum Patentänderungsantrag ein.',
'Within 2 months of service of the Defence to CCR. Includes the Rejoinder to Reply to SoD and (if applicable) the Defence to Application to amend per R.32.',
22, ARRAY['with_ccr']::text[], 'reply-to-defence-to-counterclaim-for-revocation', 'UPC.RoP.29.d')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_INF'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 5. UPC_INF cross-flow rules — wave 3 (parents = wave-2 inserts)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('inf.def_to_amend', 'inf.reply_def_amd', 'Replik auf Erwiderung zum Patentänderungsantrag', 'Reply to Defence to Application to Amend', 'claimant', 'filing', 1, 'months', 'RoP.032.3',
'Innerhalb von 1 Monat nach Zustellung der Erwiderung zum Patentänderungsantrag.',
'Within 1 month of service of the Defence to the Application to amend the patent.',
31, ARRAY['with_ccr','with_amend']::text[], 'reply-to-defence-to-application-to-amend', 'UPC.RoP.32.3'),
('inf.reply_def_ccr','inf.rejoin_reply_ccr', 'Duplik auf Replik zur Erwiderung Nichtigkeitswiderklage','Rejoinder to Reply to Defence to Counterclaim for Revocation','claimant', 'filing', 1, 'months', 'RoP.029.e',
'Innerhalb von 1 Monat nach Zustellung der Replik auf Erwiderung zur Nichtigkeitswiderklage. Beschränkt auf Erwiderung der in der Replik aufgeworfenen Punkte.',
'Within 1 month of service of the Reply to Defence to CCR. Limited to a response to matters raised in the Reply.',
32, ARRAY['with_ccr']::text[], 'rejoinder-on-reply-to-defence-to-ccr', 'UPC.RoP.29.e')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_INF'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 6. UPC_INF cross-flow rules — wave 4 (final amend rejoinder)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, 'inf.rejoin_amd', 'Duplik auf Replik zum Patentänderungsantrag', 'Rejoinder to Reply on Application to Amend',
'defendant', 'filing', true, 1, 'months', 'after', 'RoP.032.3',
'Innerhalb von 1 Monat nach Zustellung der Replik auf Erwiderung zum Patentänderungsantrag. Beschränkt auf die in der Replik aufgeworfenen Punkte.',
'Within 1 month of service of the Reply to Defence to Application to amend. Limited to matters raised in the Reply.',
33, ARRAY['with_ccr','with_amend']::text[],
(SELECT id FROM paliad.deadline_concepts WHERE slug = 'rejoinder-on-reply-to-amend'),
'UPC.RoP.32.3', false, true
FROM paliad.proceeding_types pt
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = 'inf.reply_def_amd'
WHERE pt.code = 'UPC_INF';
-- ============================================================================
-- 7. UPC_REV cross-flow rules — wave 1 (parents = pre-existing rev.defence)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('rev.defence', 'rev.app_to_amend', 'Antrag auf Patentänderung', 'Application to Amend the Patent', 'defendant', 'filing', 0, 'months', 'RoP.049.2.a',
'Optional in der Erwiderung zur Nichtigkeitsklage einzureichen (R.49.2.a). Reicht der Patentinhaber einen Patentänderungsantrag ein, eröffnet das den R.55 (= R.32 m.m.) Zyklus.',
'Optional, lodged together with the Defence to revocation per R.49.2(a). Filing this opens the R.55 (= R.32 m.m.) cycle.',
11, ARRAY['with_amend']::text[], 'application-to-amend', 'UPC.RoP.49.2.a'),
('rev.defence', 'rev.cc_inf', 'Verletzungswiderklage', 'Counterclaim for Infringement', 'defendant', 'filing', 0, 'months', 'RoP.049.2.b',
'Optional in der Erwiderung zur Nichtigkeitsklage einzuschließen (R.49.2.b, R.50). Eröffnet den R.56-Zyklus.',
'Optional, included in the Defence to revocation per R.49.2(b) and R.50. Opens the R.56 cycle.',
12, ARRAY['with_cci']::text[], 'counterclaim-for-infringement', 'UPC.RoP.49.2.b')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_REV'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 8. UPC_REV cross-flow rules — wave 2 (defences to amend / CCI)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('rev.app_to_amend', 'rev.def_to_amend', 'Erwiderung auf Patentänderungsantrag', 'Defence to Application to Amend', 'claimant', 'filing', 2, 'months', 'RoP.043.3',
'Innerhalb von 2 Monaten nach Zustellung des Patentänderungsantrags (R.43.3 i.V.m. R.55, R.32.1 m.m.).',
'Within 2 months of service of the Application to amend (R.43.3 read with R.55 and R.32.1 m.m.).',
21, ARRAY['with_amend']::text[], 'defence-to-application-to-amend', 'UPC.RoP.43.3'),
('rev.cc_inf', 'rev.def_cci', 'Erwiderung auf Verletzungswiderklage', 'Defence to Counterclaim for Infringement', 'claimant', 'filing', 2, 'months', 'RoP.056.1',
'Innerhalb von 2 Monaten nach Zustellung der Verletzungswiderklage (R.56.1).',
'Within 2 months of service of the Counterclaim for infringement (R.56.1).',
22, ARRAY['with_cci']::text[], 'defence-to-counterclaim-for-infringement','UPC.RoP.56.1')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_REV'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 9. UPC_REV cross-flow rules — wave 3 (replies)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('rev.def_to_amend', 'rev.reply_def_amd', 'Replik auf Erwiderung zum Patentänderungsantrag', 'Reply to Defence to Application to Amend', 'defendant', 'filing', 1, 'months', 'RoP.032.3',
'Innerhalb von 1 Monat nach Zustellung der Erwiderung zum Patentänderungsantrag (R.32.3 m.m.).',
'Within 1 month of service of the Defence to Application to amend (R.32.3 m.m.).',
31, ARRAY['with_amend']::text[], 'reply-to-defence-to-application-to-amend','UPC.RoP.32.3'),
('rev.def_cci', 'rev.reply_def_cci', 'Replik auf Erwiderung zur Verletzungswiderklage', 'Reply to Defence to Counterclaim for Infringement', 'defendant', 'filing', 1, 'months', 'RoP.056.3',
'Innerhalb von 1 Monat nach Zustellung der Erwiderung zur Verletzungswiderklage (R.56.3).',
'Within 1 month of service of the Defence to Counterclaim for infringement (R.56.3).',
32, ARRAY['with_cci']::text[], 'reply-to-defence-to-counterclaim-for-infringement','UPC.RoP.56.3')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_REV'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;
-- ============================================================================
-- 10. UPC_REV cross-flow rules — wave 4 (rejoinders)
-- ============================================================================
INSERT INTO paliad.deadline_rules (
proceeding_type_id, parent_id, code, name, name_en, primary_party, event_type,
is_mandatory, duration_value, duration_unit, timing, rule_code, deadline_notes, deadline_notes_en,
sequence_order, condition_flag, concept_id, legal_source, is_spawn, is_active
)
SELECT
pt.id, parent_rule.id, new.code, new.name, new.name_en, new.primary_party, new.event_type,
true, new.duration_value, new.duration_unit, 'after', new.rule_code, new.deadline_notes, new.deadline_notes_en,
new.sequence_order, new.condition_flag,
(SELECT id FROM paliad.deadline_concepts WHERE slug = new.concept_slug),
new.legal_source, false, true
FROM (VALUES
('rev.reply_def_amd','rev.rejoin_amd', 'Duplik auf Replik zum Patentänderungsantrag', 'Rejoinder to Reply on Application to Amend', 'claimant', 'filing', 1, 'months', 'RoP.032.3',
'Innerhalb von 1 Monat nach Zustellung der Replik (R.32.3 m.m.). Beschränkt auf die in der Replik aufgeworfenen Punkte.',
'Within 1 month of service of the Reply (R.32.3 m.m.). Limited to matters raised in the Reply.',
40, ARRAY['with_amend']::text[], 'rejoinder-on-reply-to-amend', 'UPC.RoP.32.3'),
('rev.reply_def_cci','rev.rejoin_cci', 'Duplik auf Replik zur Erwiderung Verletzungswiderklage', 'Rejoinder to Reply on Counterclaim for Infringement', 'claimant', 'filing', 1, 'months', 'RoP.056.4',
'Innerhalb von 1 Monat nach Zustellung der Replik (R.56.4). Beschränkt auf die in der Replik aufgeworfenen Punkte.',
'Within 1 month of service of the Reply (R.56.4). Limited to matters raised in the Reply.',
41, ARRAY['with_cci']::text[], 'rejoinder-on-counterclaim-for-infringement','UPC.RoP.56.4')
) AS new(parent_code, code, name, name_en, primary_party, event_type, duration_value, duration_unit, rule_code, deadline_notes, deadline_notes_en, sequence_order, condition_flag, concept_slug, legal_source)
JOIN paliad.proceeding_types pt ON pt.code = 'UPC_REV'
JOIN paliad.deadline_rules parent_rule ON parent_rule.proceeding_type_id = pt.id AND parent_rule.code = new.parent_code;

View File

@@ -234,47 +234,83 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
}
parentIsCourtSet := r.ParentID != nil && courtSet[*r.ParentID] && !parentOverridden
// Zero-duration rules either anchor the timeline (trigger date) or
// represent court-set waypoints with no calculable date. The court
// path covers two flavours:
// 1. zero-duration with a parent_id (waypoint chained off another
// rule, original behaviour).
// 2. zero-duration with no parent but flagged as a court-driven
// event (Zwischenverfahren / Mündliche Verhandlung /
// Entscheidung etc.) — without this, those rendered as
// IsRootEvent and emitted the trigger date as their own date,
// which then leaked into any downstream rule that chained off
// them (e.g. RoP.151 Antrag auf Kostenentscheidung).
// Zero-duration rules fall into one of four buckets:
// 1. parent=nil, not court-determined → IsRootEvent (trigger anchor)
// 2. parent=nil, court-determined → IsCourtSet (Zwischenverfahren /
// Mündliche Verhandlung / Entscheidung etc.)
// 3. parent set, court-determined → IsCourtSet (waypoint)
// 4. parent set, NOT court-determined → "filed-with-parent"
// semantic: rule is filed AT THE SAME TIME as its parent
// (e.g. UPC_REV.rev.app_to_amend, rev.cc_inf — R.49(2) says
// Application to amend / Counterclaim for infringement are
// INCLUDED in the Defence to revocation). Use the parent's
// computed date.
//
// AnchorOverrides: when the user has set a date for this court-set
// rule, surface it as a real date and propagate it as the anchor
// for downstream rules.
// AnchorOverrides: when the user has set a date for any
// zero-duration rule, that override wins over both the
// court-set placeholder and the parent-inheritance.
if r.DurationValue == 0 {
// User override always wins.
if r.Code != nil {
if ov, ok := overrideDates[*r.Code]; ok {
d.DueDate = ov.Format("2006-01-02")
d.OriginalDate = d.DueDate
d.IsOverridden = true
computed[*r.Code] = ov
deadlines = append(deadlines, d)
continue
}
}
if r.ParentID == nil && !isCourtDeterminedRule(r) {
// Bucket 1: timeline anchor.
d.IsRootEvent = true
d.DueDate = triggerDateStr
d.OriginalDate = triggerDateStr
if r.Code != nil {
computed[*r.Code] = triggerDate
}
} else if r.Code != nil {
if ov, ok := overrideDates[*r.Code]; ok {
// User has filled in this court-set date; treat as a
// real anchor. Don't apply holiday adjustment — the
// user's date is authoritative, even if it lands on
// a non-working day (real-world example: a court
// decision can be issued on a Saturday).
d.DueDate = ov.Format("2006-01-02")
d.OriginalDate = d.DueDate
d.IsOverridden = true
computed[*r.Code] = ov
} else {
} else if r.ParentID != nil && !isCourtDeterminedRule(r) {
// Bucket 4: filed-with-parent. Inherit parent's date.
// If parent is court-set, we have nothing to inherit —
// fall through to court-set marking.
if parentIsCourtSet {
d.IsCourtSet = true
d.DueDate = ""
d.OriginalDate = ""
courtSet[r.ID] = true
} else {
var parentDate time.Time
var haveParentDate bool
for _, prev := range rules {
if prev.ID == *r.ParentID {
if prev.Code != nil {
if ov, ok := overrideDates[*prev.Code]; ok {
parentDate = ov
haveParentDate = true
} else if ref, ok := computed[*prev.Code]; ok {
parentDate = ref
haveParentDate = true
}
}
break
}
}
if haveParentDate {
d.DueDate = parentDate.Format("2006-01-02")
d.OriginalDate = d.DueDate
if r.Code != nil {
computed[*r.Code] = parentDate
}
} else {
d.IsCourtSet = true
d.DueDate = ""
d.OriginalDate = ""
courtSet[r.ID] = true
}
}
} else {
// Buckets 2 + 3: court-determined.
d.IsCourtSet = true
d.DueDate = ""
d.OriginalDate = ""